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.
This commit is contained in:
2026-05-23 19:10:29 +08:00
parent 81c326af53
commit a1b4e629aa
165 changed files with 7904 additions and 313 deletions

View File

@@ -0,0 +1,189 @@
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
{
[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>能力被外部中断时触发BD Task / 状态机订阅用)。</summary>
public event System.Action<InterruptReason> Interrupted;
/// <summary>BD 任务统一查询入口:当前是否可用(冷却完毕且未执行中)。</summary>
public virtual bool CanUse => !_isRunning && !IsOnCooldown && _enemy != null && _enemy.IsAlive;
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>子类辅助:朝向目标。</summary>
protected void FaceTarget(Transform target)
{
if (target == null || _enemy == null) return;
float dx = target.position.x - _transform.position.x;
if (Mathf.Abs(dx) < 0.001f) return;
var s = _transform.localScale;
s.x = Mathf.Abs(s.x) * Mathf.Sign(dx);
_transform.localScale = s;
}
}
/// <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;
}
}
}