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,294 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.ResourceManagement.ResourceProviders;
using UnityEngine.SceneManagement;
namespace BaseGames.World.Streaming
{
/// <summary>
/// 单个房间的流式加载状态包装器。
/// <para>
/// 由 <see cref="RoomStreamingManager"/> 创建和管理,封装以下职责:
/// <list type="bullet">
/// <item>Addressables 加载 / 卸载句柄</item>
/// <item>Dormantize批量关闭 Renderer、暂停 IRoomLifecycleAI、音效等</item>
/// <item>Activate批量恢复渲染分帧唤醒 IRoomLifecycle</item>
/// <item>Deactivate玩家离开后关闭本房间内容</item>
/// </list>
/// </para>
/// </summary>
public class RoomHandle
{
// ── 公开属性 ──────────────────────────────────────────────────────────────
public string RoomId { get; }
public RoomState State { get; private set; } = RoomState.Unloaded;
public int EstimatedMemKB { get; set; }
/// <summary>上次处于 Active 状态的时间Time.time。用于 LRU 卸载优先级计算。</summary>
public float LastActiveTime { get; private set; }
/// <summary>加载完成后由 RoomController.Start() 通过 IRoomStreamingManager.RegisterRoomController 注入。</summary>
public RoomController RoomController { get; set; }
// ── 私有字段 ──────────────────────────────────────────────────────────────
private AsyncOperationHandle<SceneInstance> _sceneHandle;
// 缓存的场景组件集合Dormantize 后可快速 Activate
private Renderer[] _renderers;
private IRoomLifecycle[] _lifecycles;
private AudioSource[] _audioSources;
private Light[] _lights;
private ParticleSystem[] _particleSystems;
// 分帧激活的协程宿主(由 RoomStreamingManager 提供)
private MonoBehaviour _coroutineRunner;
private int _lifecycleActivatePerFrame;
// ── 构造 ──────────────────────────────────────────────────────────────────
public RoomHandle(string roomId, MonoBehaviour coroutineRunner, int lifecycleActivatePerFrame)
{
RoomId = roomId;
_coroutineRunner = coroutineRunner;
_lifecycleActivatePerFrame = lifecycleActivatePerFrame;
}
// ── 加载 / 卸载 ───────────────────────────────────────────────────────────
/// <summary>
/// 开始后台异步加载Additive
/// 加载完成后自动调用 <see cref="Dormantize"/> 将房间置于休眠状态。
/// </summary>
public IEnumerator LoadAsync(string addressableKey)
{
if (State != RoomState.Unloaded)
{
Debug.LogWarning($"[RoomHandle] {RoomId} 非 Unloaded 状态,忽略重复加载请求。");
yield break;
}
State = RoomState.Loading;
_sceneHandle = Addressables.LoadSceneAsync(addressableKey, LoadSceneMode.Additive);
while (!_sceneHandle.IsDone)
yield return null;
if (_sceneHandle.Status != AsyncOperationStatus.Succeeded)
{
Debug.LogError($"[RoomHandle] {RoomId} 加载失败:{addressableKey}");
State = RoomState.Unloaded;
yield break;
}
// 收集场景内组件引用,然后置于休眠
CollectSceneComponents();
Dormantize();
}
/// <summary>异步卸载本房间。完成后 State → Unloaded。</summary>
public IEnumerator UnloadAsync()
{
if (State == RoomState.Unloaded || State == RoomState.Unloading || !_sceneHandle.IsValid()) yield break;
// Active 状态先走 Deactivate再设置 Unloading
bool needsDeactivate = State == RoomState.Active ||
State == RoomState.Activating ||
State == RoomState.Cooling;
if (needsDeactivate)
Deactivate();
State = RoomState.Unloading;
var op = Addressables.UnloadSceneAsync(_sceneHandle);
yield return op;
_renderers = null;
_lifecycles = null;
_audioSources = null;
_lights = null;
_particleSystems = null;
RoomController = null;
State = RoomState.Unloaded;
}
// ── Dormantize ────────────────────────────────────────────────────────────
/// <summary>
/// 将已加载的房间置于休眠状态。
/// 关闭所有 Renderer 和 AudioSource通知所有 IRoomLifecycle 组件进入休眠。
/// 场景内 GameObject 保持激活(便于快速恢复),但不消耗渲染和 AI 开销。
/// </summary>
public void Dormantize()
{
// 允许从 Loading初次加载完成后、Active、Activating、Cooling 进入
if (State != RoomState.Loading &&
State != RoomState.Active &&
State != RoomState.Activating &&
State != RoomState.Cooling)
{
Debug.LogWarning($"[RoomHandle] {RoomId} 在 {State} 状态下调用 Dormantize忽略。");
return;
}
if (_renderers != null)
foreach (var r in _renderers) if (r != null) r.enabled = false;
if (_audioSources != null)
foreach (var a in _audioSources) if (a != null) { a.Stop(); a.enabled = false; }
if (_lights != null)
foreach (var l in _lights) if (l != null) l.enabled = false;
if (_particleSystems != null)
foreach (var ps in _particleSystems) if (ps != null) ps.Pause();
if (_lifecycles != null)
foreach (var l in _lifecycles) l?.OnRoomDormant();
State = RoomState.Dormant;
}
// ── Activate ──────────────────────────────────────────────────────────────
/// <summary>
/// 激活此房间Dormant → Active
/// 恢复 Renderer、AudioSource分帧唤醒 IRoomLifecycle并设置玩家出生点。
/// 目标房间必须已处于 <see cref="RoomState.Dormant"/> 状态。
/// </summary>
public IEnumerator Activate(SpawnContext context)
{
if (State != RoomState.Dormant)
{
Debug.LogError($"[RoomHandle] {RoomId} 不是 Dormant 状态(当前:{State}),无法激活。");
yield break;
}
State = RoomState.Activating;
// 先恢复渲染(让玩家立刻能看到新房间)
if (_renderers != null)
foreach (var r in _renderers) if (r != null) r.enabled = true;
if (_audioSources != null)
foreach (var a in _audioSources) if (a != null) a.enabled = true;
if (_lights != null)
foreach (var l in _lights) if (l != null) l.enabled = true;
if (_particleSystems != null)
foreach (var ps in _particleSystems) if (ps != null) ps.Play();
// 相机 + 出生点(由 RoomController 处理)
RoomController?.OnRoomActivate(context);
// 分帧激活 IRoomLifecycleAI、特效等
yield return _coroutineRunner.StartCoroutine(ActivateLifecyclesGradually(context));
State = RoomState.Active;
LastActiveTime = Time.time;
}
/// <summary>将 IRoomLifecycle 组件分帧激活,避免单帧 CPU 峰值。</summary>
private IEnumerator ActivateLifecyclesGradually(SpawnContext context)
{
if (_lifecycles == null) yield break;
int activatedThisFrame = 0;
foreach (var lc in _lifecycles)
{
if (lc == null) continue;
lc.OnRoomActivate(context);
activatedThisFrame++;
if (activatedThisFrame >= _lifecycleActivatePerFrame)
{
activatedThisFrame = 0;
yield return null;
}
}
}
// ── Deactivate ────────────────────────────────────────────────────────────
/// <summary>
/// 玩家离开后关闭此房间Active / Activating → Dormant
/// 与 <see cref="Dormantize"/> 相同,但语义上表示从 Active 退出。
/// </summary>
public void Deactivate()
{
if (State != RoomState.Active && State != RoomState.Activating && State != RoomState.Cooling)
{
Debug.LogWarning($"[RoomHandle] {RoomId} 在 {State} 状态下调用 Deactivate忽略。");
return;
}
Dormantize();
}
// ── 冷却 ──────────────────────────────────────────────────────────────────
/// <summary>
/// 标记为冷却中,同时休眠渲染和 AI 开销。
/// 玩家离开后不立即卸载,等待冷却计时结束后再卸载。
/// </summary>
public void BeginCooling()
{
if (State != RoomState.Active && State != RoomState.Activating) return;
// 先休眠渲染和 AI与 Dormantize 相同,但最终 State 设为 Cooling 而非 Dormant
Dormantize();
State = RoomState.Cooling;
}
/// <summary>
/// 冷却期间玩家返回,将状态从 Cooling 重置为 Dormant。
/// 渲染已在 BeginCooling 时关闭,无需重复操作。
/// </summary>
public void ResetToDormant()
{
if (State == RoomState.Cooling)
State = RoomState.Dormant;
}
// ── 内部工具 ──────────────────────────────────────────────────────────────
/// <summary>
/// 收集加载完成的场景中所有需要被管理的组件引用。
/// 在 LoadAsync 完成后、Dormantize 之前调用一次。
/// </summary>
private void CollectSceneComponents()
{
if (!_sceneHandle.IsValid()) return;
Scene scene = _sceneHandle.Result.Scene;
GameObject[] roots = scene.GetRootGameObjects();
var rendererList = new List<Renderer>();
var lifecycleList = new List<IRoomLifecycle>();
var audioSourceList = new List<AudioSource>();
var lightList = new List<Light>();
var particleList = new List<ParticleSystem>();
foreach (var root in roots)
{
rendererList.AddRange(root.GetComponentsInChildren<Renderer>(true));
lifecycleList.AddRange(root.GetComponentsInChildren<IRoomLifecycle>(true));
audioSourceList.AddRange(root.GetComponentsInChildren<AudioSource>(true));
lightList.AddRange(root.GetComponentsInChildren<Light>(true));
particleList.AddRange(root.GetComponentsInChildren<ParticleSystem>(true));
}
_renderers = rendererList.ToArray();
_lifecycles = lifecycleList.ToArray();
_audioSources = audioSourceList.ToArray();
_lights = lightList.ToArray();
_particleSystems = particleList.ToArray();
}
public override string ToString() => $"RoomHandle[{RoomId}, {State}]";
}
}