Files
zeling_v2/Assets/_Game/Scripts/Enemies/Boss/BossSkillExecutor.cs
Joywayer a1b4e629aa feat: Implement Room Streaming System
- Add RoomStreamingManager to manage room loading and unloading based on player proximity.
- Create StreamingBudgetConfigSO for memory and performance budgeting of the streaming system.
- Introduce TransitionDirector to handle seamless and atmospheric fade transitions between rooms.
- Develop WorldGraph to represent room connectivity and facilitate neighbor queries and distance calculations.
- Implement RoomNode and RoomEdge classes to structure room data and connections.
2026-05-23 19:10:29 +08:00

391 lines
16 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 System.Collections.Generic;
using UnityEngine;
using Animancer;
using BaseGames.Combat;
using BaseGames.Core.Events;
namespace BaseGames.Boss
{
/// <summary>
/// 挂在 Boss GameObject 上,接收 BossOrchestrator 的指令执行指定 BossSkillSO。
/// 管理 VulnerabilityWindow 计时和 WeakPointSystem 激活。
/// </summary>
public class BossSkillExecutor : MonoBehaviour
{
[SerializeField] private HitBox[] _hitBoxes;
[SerializeField] private WeakPointSystem _weakPointSystem;
[SerializeField] private AnimancerComponent _animancer;
[SerializeField] private string _bossId;
[SerializeField] private BossSkillEventChannelSO _onBossSkillStarted;
[SerializeField] private BossSkillEventChannelSO _onBossSkillEnded;
/// <remarks>PlayerController 无 Instance架构 05 §2由 Inspector 指定。</remarks>
[SerializeField] private Transform _playerTransform;
[SerializeField] private BossSkillSO[] _skills;
[Header("技能重复检测范围")]
[Tooltip("SkillSequence RepeatIfPlayerInRange 的检测半径m")]
[SerializeField, Min(1f)] private float _repeatRangeCheck = 8f;
private BossSkillSO _currentSkill;
private bool _isExecuting;
private Coroutine _activeCoroutine;
private Coroutine _vulnCoroutine; // 弱点窗口协程(中断时需同步停止)
private bool _patternHitConfirmed; // 本次技能执行期间是否有 HitBox 命中
// 技能冷却skillId → 冷却结束的 Time.time 时刻
private readonly Dictionary<string, float> _skillCooldownEndTimes = new();
public bool IsExecuting => _isExecuting;
/// <summary>检查指定技能是否冷却就绪(无冷却记录或已过冷却时间)。</summary>
public bool CanUseSkill(string skillId)
{
if (string.IsNullOrEmpty(skillId)) return false;
if (_skillCooldownEndTimes.TryGetValue(skillId, out float endTime))
return Time.time >= endTime;
return true;
}
/// <summary>强制重置指定技能的冷却(阶段切换、复活等场景使用)。</summary>
public void ResetSkillCooldown(string skillId)
{
_skillCooldownEndTimes.Remove(skillId);
}
/// <summary>重置所有技能冷却。</summary>
public void ResetAllCooldowns() => _skillCooldownEndTimes.Clear();
private void Awake()
{
#if UNITY_EDITOR
ValidateSkillConfig();
#endif
}
#if UNITY_EDITOR
private void OnValidate() => ValidateSkillConfig();
private void ValidateSkillConfig()
{
if (_skills == null || _skills.Length == 0)
{
Debug.LogError($"[BossSkillExecutor] Boss '{_bossId}' ({gameObject.name}) 未配置任何技能 SO。", this);
return;
}
foreach (var skill in _skills)
{
if (skill == null)
Debug.LogError($"[BossSkillExecutor] Boss '{_bossId}' ({gameObject.name}) _skills 数组含 null 元素。", this);
else if (string.IsNullOrEmpty(skill.skillId))
Debug.LogError($"[BossSkillExecutor] Boss '{_bossId}' ({gameObject.name}) 技能 '{skill.name}' 缺少 skillId。", this);
}
}
#endif
/// <summary>
/// 按 float 值复用 WaitForSeconds 实例,消除协程中每次 new WaitForSeconds 的 GC 分配。
/// Domain Reload 禁用时静态缓存跨 PlayMode 会话保留,但 WaitForSeconds 是幂等值对象,
/// 不会引发功能错误;[RuntimeInitializeOnLoadMethod] 确保每次进入 Play 时清空。
/// </summary>
private static readonly Dictionary<float, WaitForSeconds> _wfsCache = new();
[UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.SubsystemRegistration)]
private static void ClearWFSCache() => _wfsCache.Clear();
private const int MaxWFSCacheSize = 64;
private static WaitForSeconds GetWFS(float t)
{
if (!_wfsCache.TryGetValue(t, out var wfs))
{
if (_wfsCache.Count < MaxWFSCacheSize)
_wfsCache[t] = wfs = new WaitForSeconds(t);
else
return new WaitForSeconds(t);
}
return wfs;
}
// ── 公共 API ───────────────────────────────────────────────────────────
/// <summary>
/// 按 skillId 查找已在 Inspector 注册的技能 SO。未找到返回 null。
/// </summary>
public BossSkillSO FindSkill(string skillId)
{
if (_skills == null) return null;
foreach (var s in _skills)
if (s != null && s.skillId == skillId) return s;
return null;
}
/// <summary>返回当前正在执行的技能 SO未执行时返回 null。</summary>
public BossSkillSO FindCurrentSkill() => _isExecuting ? _currentSkill : null;
/// <summary>Inspector 中注册的全部技能 SO只读。</summary>
public BossSkillSO[] Skills => _skills;
/// <summary>
/// 从候选技能列表中按 weight 加权随机选择一个技能。
/// 权重为 0 的技能不参与选择;所有候选权重均为 0 时返回 null。
/// </summary>
public BossSkillSO SelectWeightedSkill(System.Collections.Generic.IList<BossSkillSO> candidates)
{
if (candidates == null || candidates.Count == 0) return null;
float totalWeight = 0f;
for (int i = 0; i < candidates.Count; i++)
{
var s = candidates[i];
if (s != null && s.weight > 0f) totalWeight += s.weight;
}
if (totalWeight <= 0f) return null;
float roll = UnityEngine.Random.value * totalWeight;
float acc = 0f;
for (int i = 0; i < candidates.Count; i++)
{
var s = candidates[i];
if (s == null || s.weight <= 0f) continue;
acc += s.weight;
if (roll <= acc) return s;
}
// 浮点精度兜底:返回最后一个有效候选
for (int i = candidates.Count - 1; i >= 0; i--)
if (candidates[i]?.weight > 0f) return candidates[i];
return null;
}
/// <summary>
/// 执行一个 Boss 技能。若当前正在执行或技能冷却未就绪则返回。
/// </summary>
public void ExecuteSkill(BossSkillSO skill)
{
if (_isExecuting || skill == null) return;
if (!CanUseSkill(skill.skillId))
{
Debug.Log($"[BossSkillExecutor] 技能 '{skill.skillId}' 冷却中,无法执行。", this);
return;
}
// 提前订阅,确保 InterruptCurrentSkill() 中断时 FinishExecution() 能正常取消
SubscribeHitCallbacks();
_activeCoroutine = StartCoroutine(ExecuteSkillCoroutine(skill));
}
/// <summary>
/// 立即打断正在执行的技能(阶段切换时调用)。
/// </summary>
public void InterruptCurrentSkill()
{
// 同步停止弱点窗口协程,防止中断后继续激活 WeakPointSystem
if (_vulnCoroutine != null)
{
StopCoroutine(_vulnCoroutine);
_vulnCoroutine = null;
}
if (_activeCoroutine != null)
{
StopCoroutine(_activeCoroutine);
_activeCoroutine = null;
}
FinishExecution();
}
// 等待事件触发的 VulnWindow事件驱动类型存储后由 NotifyVulnTrigger 逐个激活)
private readonly List<VulnerabilityWindow> _pendingEventWindows = new();
/// <summary>
/// 通知执行器某一外部事件已发生(如格挡成功、反制命中等),
/// 激活所有注册该触发类型的弱点窗口。
/// 由 BossBase.HandleParrySuccess / ApplyCounterResponse 等调用。
/// </summary>
public void NotifyVulnTrigger(VulnTriggerType triggerType)
{
for (int i = _pendingEventWindows.Count - 1; i >= 0; i--)
{
var w = _pendingEventWindows[i];
if (w.TriggerType == triggerType)
{
_pendingEventWindows.RemoveAt(i);
StartCoroutine(OpenWindowCoroutine(w));
}
}
}
private void OnHitConfirmedCallback(DamageInfo _) => _patternHitConfirmed = true;
private void SubscribeHitCallbacks()
{
if (_hitBoxes == null) return;
foreach (var hb in _hitBoxes) if (hb != null) hb.OnHitConfirmed += OnHitConfirmedCallback;
}
private void UnsubscribeHitCallbacks()
{
if (_hitBoxes == null) return;
foreach (var hb in _hitBoxes) if (hb != null) hb.OnHitConfirmed -= OnHitConfirmedCallback;
}
private IEnumerator ExecuteSkillCoroutine(BossSkillSO skill)
{
_isExecuting = true;
_currentSkill = skill;
_patternHitConfirmed = false;
// HitBox 订阅已在 ExecuteSkill() 入口完成(确保 Interrupt 中断时能在 FinishExecution 取消)
_onBossSkillStarted?.Raise(new BossSkillEvent { BossId = _bossId, SkillId = skill.skillId });
// 播放技能动画
if (skill.skillAnimation != null)
_animancer.Play(skill.skillAnimation);
// 启动 VulnerabilityWindow 协程(与主序列并行)
_vulnCoroutine = null;
if (skill.vulnerabilityWindows != null && skill.vulnerabilityWindows.Length > 0)
_vulnCoroutine = StartCoroutine(ActivateVulnerabilityWindowsCoroutine(skill));
// 执行主攻击序列(始终执行 sequenceOnMisssequenceOnHit 是命中后的追加序列)
if (skill.sequenceOnMiss != null)
yield return ExecuteSequenceCoroutine(skill.sequenceOnMiss);
// 若本次有命中确认且配置了 sequenceOnHit执行追加序列连段、击倒追击等
if (_patternHitConfirmed && skill.sequenceOnHit != null)
yield return ExecuteSequenceCoroutine(skill.sequenceOnHit);
// 若弱点协程还在运行则等待其结束(避免孤立协程)
if (_vulnCoroutine != null)
yield return _vulnCoroutine;
FinishExecution();
}
private void FinishExecution()
{
UnsubscribeHitCallbacks(); // 无论正常结束还是被 Interrupt均在此取消订阅
_pendingEventWindows.Clear(); // 清除未触发的事件驱动弱点窗口,防止跨技能积压
_vulnCoroutine = null; // 正常结束时已自然结束,仅清除引用
_isExecuting = false;
if (_currentSkill != null)
{
// 记录冷却结束时刻
if (_currentSkill.cooldown > 0f)
_skillCooldownEndTimes[_currentSkill.skillId] = Time.time + _currentSkill.cooldown;
_onBossSkillEnded?.Raise(new BossSkillEvent { BossId = _bossId, SkillId = _currentSkill.skillId });
_currentSkill = null;
}
}
// ── 序列协程 ────────────────────────────────────────────────────────────
private IEnumerator ExecuteSequenceCoroutine(SkillSequenceSO seq)
{
int repeatCount = 0;
do
{
foreach (var step in seq.steps)
{
if (step.delayBeforeStep > 0f)
yield return GetWFS(step.delayBeforeStep);
if (step.pattern != null)
yield return ExecutePatternCoroutine(step.pattern);
}
repeatCount++;
if (seq.RepeatIfPlayerInRange && seq.RepeatDelay > 0f)
yield return GetWFS(seq.RepeatDelay);
}
while (seq.RepeatIfPlayerInRange
&& (seq.MaxRepeatCount == 0 || repeatCount < seq.MaxRepeatCount)
&& IsPlayerInRange());
}
private IEnumerator ExecutePatternCoroutine(AttackPatternSO pattern)
{
// 预备
if (pattern.WindupDuration > 0f)
yield return GetWFS(pattern.WindupDuration);
// 激活 HitBox架构 06 §4Activate(DamageSourceSO, Transform)
if (_hitBoxes != null && _hitBoxes.Length > 0)
foreach (var hb in _hitBoxes)
if (hb != null) hb.Activate(pattern.DamageSource, transform);
if (pattern.ActiveDuration > 0f)
yield return GetWFS(pattern.ActiveDuration);
// 关闭 HitBox
if (_hitBoxes != null && _hitBoxes.Length > 0)
foreach (var hb in _hitBoxes)
if (hb != null) hb.Deactivate();
// 后摇
if (pattern.RecoveryDuration > 0f)
yield return GetWFS(pattern.RecoveryDuration);
}
// ── VulnerabilityWindow 协程 ─────────────────────────────────────────────
private IEnumerator ActivateVulnerabilityWindowsCoroutine(BossSkillSO skill)
{
_pendingEventWindows.Clear();
foreach (var window in skill.vulnerabilityWindows)
{
if (window.TriggerType == VulnTriggerType.OnAttackRecovery)
{
// 时间驱动:按 TriggerDelay 延迟后自动激活
if (window.TriggerDelay > 0f)
yield return GetWFS(window.TriggerDelay);
StartCoroutine(OpenWindowCoroutine(window));
}
else
{
// 事件驱动OnParriedSuccess / Manual 等):注册到待触发列表
_pendingEventWindows.Add(window);
}
}
}
/// <summary>实际开启并持续弱点窗口,支持独立并行运行。</summary>
private IEnumerator OpenWindowCoroutine(VulnerabilityWindow window)
{
bool activateSpecific = window.ActivateWeakPointHurtBox;
_weakPointSystem?.SetActive(true, window.DamageMultiplier, activateSpecific);
window.OpenFeedback?.Play();
yield return GetWFS(window.Duration);
_weakPointSystem?.SetActive(false, 1f, activateSpecific);
window.CloseFeedback?.Play();
}
// ── 工具 ───────────────────────────────────────────────────────────────
/// <summary>
/// 在指定时长内开启弱点窗口(格挡/闪避反制时调用,独立于技能 VulnerabilityWindow 序列)。
/// </summary>
public void OpenVulnerabilityWindow(float duration, float damageMultiplier)
{
if (_weakPointSystem == null || duration <= 0f) return;
StartCoroutine(VulnWindowOverride(duration, damageMultiplier));
}
private IEnumerator VulnWindowOverride(float duration, float multiplier)
{
_weakPointSystem.SetActive(true, multiplier, false);
yield return GetWFS(duration);
_weakPointSystem.SetActive(false, 1f, false);
}
private bool IsPlayerInRange() =>
_playerTransform != null &&
Vector2.Distance(transform.position, _playerTransform.position) < _repeatRangeCheck;
}
}