Files
zeling_v2/Assets/_Game/Scripts/Enemies/Abilities/EnemyAbilityBase.cs
T
joywayer 64d2087adc fix(enemy): 禁用的能力组件不再报告可用
CanUse 纳入 enabled。此前禁用组件仍返回 true,选招器会选中一个
StartCoroutine 必然失败的招。这也是阶段门(按阶段启停能力)的前提。
2026-07-30 13:28:23 +08:00

232 lines
11 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 System.Collections;
using UnityEngine;
using Animancer;
using BaseGames.Core;
using BaseGames.Core.Pool;
namespace BaseGames.Enemies.Abilities
{
/// <summary>
/// 敌人能力抽象基类(架构 07_EnemyModule §8.4)。
/// 责任:生命周期管理、冷却计时、中断分发。子类只实现 <see cref="ExecuteCoroutine"/>。
///
/// 设计要点:
/// - 单一执行实例:同一能力同时只有一个协程在跑。
/// - 协程内 yield WaitForSeconds 复用 <see cref="EnemyAbilityWaits"/>(无 GC)。
/// - 受击/死亡时由 <see cref="EnemyBase"/> 调用 <see cref="Interrupt"/>。
/// </summary>
[DisallowMultipleComponent]
public abstract class EnemyAbilityBase : MonoBehaviour, IAttackCandidate
{
[Header("配置 SO")]
[SerializeField] protected EnemyAbilitySO _config;
// 缓存依赖(Awake 填入,热路径无 GetComponent
protected EnemyBase _enemy;
protected AnimancerComponent _animancer;
protected Transform _transform;
private Coroutine _runner;
private float _cooldownEndTime = -1f;
private bool _isRunning;
// ── 公共状态 ─────────────────────────────────────────────────────
public EnemyAbilitySO Config => _config;
public bool IsRunning => _isRunning;
public AbilityRunState Phase { get; protected set; } = AbilityRunState.Idle;
public float CooldownRemaining => Mathf.Max(0f, _cooldownEndTime - Time.time);
public bool IsOnCooldown => CooldownRemaining > 0f;
/// <summary>能力被外部中断时触发(AI 决策层 / 状态机订阅用)。</summary>
public event System.Action<InterruptReason> Interrupted;
/// <summary>
/// 统一可用性查询:组件启用 + 冷却完毕 + 未执行中 + 宿主存活。
/// enabled 这一维供阶段门使用——被阶段禁用的能力不得进入选招候选,
/// 否则选招器会选中它然后 StartCoroutine 在禁用组件上必然失败。
/// </summary>
public virtual bool CanUse => enabled && !_isRunning && !IsOnCooldown
&& _enemy != null && _enemy.IsAlive;
// ── IAttackCandidate(供 EnemyAttackSelector 选招)──────────────────
public bool RequiresLineOfSight => _config != null && _config.requiresLineOfSight;
public bool RequiresGrounded => _config != null && _config.requiresGrounded;
public float Weight => _config != null ? _config.weight : 0f;
public int Priority => _config != null ? _config.priority : 0;
/// <summary>本招圆形攻击范围内是否有玩家(招式自管其射程;LOS 复用敌人级 IsPlayerVisible)。</summary>
public virtual bool InAttackRange()
{
if (_config == null || _config.rangeRadius <= 0f
|| _enemy == null || _enemy.PlayerTransform == null) return false;
float sign = _transform.localScale.x < 0f ? -1f : 1f;
Vector2 origin = (Vector2)_transform.position
+ new Vector2(_config.rangeOffset.x * sign, _config.rangeOffset.y);
float r = _config.rangeRadius;
return ((Vector2)_enemy.PlayerTransform.position - origin).sqrMagnitude <= r * r;
}
protected virtual void Awake()
{
_enemy = GetComponentInParent<EnemyBase>();
_animancer = _enemy != null ? _enemy.Animancer : GetComponentInParent<AnimancerComponent>();
_transform = transform;
if (_enemy == null)
Debug.LogError($"[EnemyAbilityBase] {GetType().Name} 找不到 EnemyBase。", this);
if (_animancer == null)
Debug.LogWarning($"[EnemyAbilityBase] {GetType().Name} 找不到 AnimancerComponent,动画能力将无法播放动画。", this);
}
protected virtual void OnDisable()
{
if (_isRunning) Interrupt(InterruptReason.ExternalRequest);
}
// ── 执行 ─────────────────────────────────────────────────────────
/// <summary>
/// 启动能力。重复调用、冷却中或已运行将返回 false。
/// 若 <see cref="EnemyAbilitySO.exclusionGroup"/> 非空,会先中断同组其他能力(互斥)。
/// </summary>
public bool Execute()
{
if (!CanUse) return false;
// 互斥组:启动前中断同组正在运行的其他能力
if (_config != null && !string.IsNullOrEmpty(_config.exclusionGroup))
_enemy?.Abilities.InterruptGroup(_config.exclusionGroup, InterruptReason.ExternalRequest);
_runner = StartCoroutine(RunInternal());
return true;
}
/// <summary>
/// 强制启动能力,忽略冷却检查(连段语义:外部组合技调用子能力时使用)。
/// 若能力正在运行则先中断再重启。
/// </summary>
public bool ForceExecute()
{
if (_enemy == null || !_enemy.IsAlive) return false;
if (_isRunning) Interrupt(InterruptReason.ExternalRequest);
_runner = StartCoroutine(RunInternal());
return true;
}
private IEnumerator RunInternal()
{
_isRunning = true;
Phase = AbilityRunState.Telegraph;
try
{
if (_config != null && _config.telegraphDuration > 0f)
yield return TelegraphRoutine();
Phase = AbilityRunState.Windup;
yield return ExecuteCoroutine();
Phase = AbilityRunState.Recovery;
}
finally
{
_isRunning = false;
_runner = null;
_cooldownEndTime = Time.time + (_config != null ? _config.cooldown : 0f);
if (Phase != AbilityRunState.Interrupted) Phase = AbilityRunState.Idle;
OnAbilityEnded();
}
}
/// <summary>子类实现:能力主体。可分多段、含 HitBox 激活/弹幕生成/物理推进等。</summary>
protected abstract IEnumerator ExecuteCoroutine();
/// <summary>预警阶段(默认生成 VFX 后等待 telegraphDuration)。子类可重写。</summary>
protected virtual IEnumerator TelegraphRoutine()
{
if (!string.IsNullOrEmpty(_config.telegraphVfxKey))
{
var pool = ServiceLocator.GetOrDefault<IObjectPoolService>();
pool?.Spawn(_config.telegraphVfxKey, _transform.position, Quaternion.identity);
}
yield return EnemyAbilityWaits.Get(_config.telegraphDuration);
}
/// <summary>能力结束钩子(被中断或正常结束都会调用)。</summary>
protected virtual void OnAbilityEnded() { }
/// <summary>中断当前执行。冷却仍会按配置计入。</summary>
public void Interrupt(InterruptReason reason)
{
if (!_isRunning) return;
if (_config != null)
{
if (reason == InterruptReason.Hurt && !_config.interruptOnHurt) return;
if (reason == InterruptReason.Stagger && !_config.interruptOnStagger) return;
}
if (_runner != null) StopCoroutine(_runner);
_runner = null;
_isRunning = false;
Phase = AbilityRunState.Interrupted;
OnInterrupted(reason);
Interrupted?.Invoke(reason);
OnAbilityEnded();
_cooldownEndTime = Time.time + (_config != null ? _config.cooldown * 0.5f : 0f);
}
protected virtual void OnInterrupted(InterruptReason reason) { }
/// <summary>子类辅助:朝向目标(写入输入信号,下一 FixedUpdate 由 EnemyMovement 消费)。</summary>
protected void FaceTarget(Transform target)
{
if (target == null || _enemy == null) return;
_enemy.FaceTarget(target.position);
}
/// <summary>把 _config 解析为期望的子类型;类型不符/为空时显式报错(不回退)。</summary>
protected T ResolveConfig<T>() where T : EnemyAbilitySO
{
if (_config is T typed) return typed;
Debug.LogError($"[{GetType().Name}] _config 需为 {typeof(T).Name}" +
$"实际 = {(_config != null ? _config.GetType().Name : "null")}。请重建为正确的子类 SO。", this);
return null;
}
#if UNITY_EDITOR
/// <summary>选中时绘制攻击射程范围(编辑器可视化辅助,不影响运行时)。</summary>
private void OnDrawGizmosSelected()
{
if (_config == null || _config.category != AbilityCategory.Attack || _config.rangeRadius <= 0f) return;
float sign = transform.localScale.x < 0f ? -1f : 1f;
Vector3 origin = transform.position
+ new Vector3(_config.rangeOffset.x * sign, _config.rangeOffset.y, 0f);
UnityEditor.Handles.color = new Color(1f, 0.4f, 0.2f, 0.9f);
UnityEditor.Handles.DrawWireDisc(origin, Vector3.forward, _config.rangeRadius);
UnityEditor.Handles.Label(origin, $"{_config.abilityId} range");
}
#endif
}
/// <summary>WaitForSeconds 池(架构 §10 GC 优化)。能力协程统一通过此获取等待指令。</summary>
internal static class EnemyAbilityWaits
{
private const int MaxCacheSize = 64;
private static readonly System.Collections.Generic.Dictionary<float, WaitForSeconds> _cache
= new System.Collections.Generic.Dictionary<float, WaitForSeconds>(32);
public static WaitForSeconds Get(float seconds)
{
if (seconds <= 0f) return null;
if (!_cache.TryGetValue(seconds, out var w))
{
if (_cache.Count < MaxCacheSize)
{
w = new WaitForSeconds(seconds);
_cache[seconds] = w;
}
else
{
return new WaitForSeconds(seconds);
}
}
return w;
}
}
}