- 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.
391 lines
16 KiB
C#
391 lines
16 KiB
C#
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));
|
||
|
||
// 执行主攻击序列(始终执行 sequenceOnMiss;sequenceOnHit 是命中后的追加序列)
|
||
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 §4:Activate(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;
|
||
}
|
||
}
|