using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using BaseGames.Core;
using BaseGames.Core.Events;
using BaseGames.World.Map;
namespace BaseGames.World.Streaming
{
///
/// 核心流式加载调度器。挂载于 Persistent 场景的 SYS_RoomStreamingManager 对象上。
///
/// 职责:
///
/// - 在 Persistent 场景初始化时从 MapDatabaseSO 构建
/// - 监听 事件,重新计算 StreamingSet
/// - 后台异步预加载邻居房间(),并发数由预算配置限制
/// - 执行内存预算检查,通过 LRU 策略卸载距离最远、最久未访问的 Dormant 房间
/// - 提供 ActivateRoom / DeactivateRoom 接口供 调用
///
///
///
[DefaultExecutionOrder(-800)]
public class RoomStreamingManager : MonoBehaviour, IRoomStreamingManager, ISceneLoadCoordinator
{
[Header("配置")]
[SerializeField] private MapDatabaseSO _mapDatabase;
[SerializeField] private StreamingBudgetConfigSO _budget;
[Header("事件频道 - 监听(输入)")]
[Tooltip("外部触发器或非流式加载路径在玩家到达新房间时发布此事件(携带 RoomId)。\n" +
"流式管理器订阅此频道以支持冷启动和外部调试传送。\n" +
"正常流式过渡路径由 ActivateRoomCoroutine 直接更新 _currentRoomId,不依赖此订阅。")]
[SerializeField] private StringEventChannelSO _onRoomEntered;
[Header("事件频道 - 发布")]
[Tooltip("某个房间加载并进入 Dormant 状态后发布(携带 RoomId)。\n" +
"供 TransitionDirector 检查是否可执行 Seamless 切换。")]
[SerializeField] private StringEventChannelSO _onRoomPreloaded;
[Tooltip("房间激活完成(Active)后发布(携带 RoomId)。\n" +
"地图探索、任务系统等应订阅此事件以感知当前房间变更。\n" +
"注意:与 _onRoomEntered(输入)是不同频道,避免流式管理器自身产生事件循环。")]
[SerializeField] private StringEventChannelSO _onRoomActivated;
[Header("格子单位(世界坐标)")]
[Tooltip("每个格子对应的 Unity 世界坐标单位数,与关卡设计网格对齐。")]
[SerializeField] private float _unitsPerGrid = 16f;
// ── 运行时状态 ─────────────────────────────────────────────────────────────
private WorldGraph _graph;
private Dictionary _handles = new();
private readonly Queue _loadQueue = new();
/// O(1) Contains 镜像,与 _loadQueue 保持同步,避免 Queue.Contains 的 O(n) 开销。
private readonly HashSet _queuedRoomIds = new();
/// 当前正在运行 CoolingCoroutine 的房间,防止竞态下重复启动冷却协程。
private readonly HashSet _roomsInCooling = new();
/// 单次 BFS 预算:从当前房间到所有已加载房间的跳数缓存,供 LRU 排序使用。
private Dictionary _hopCache = new();
private int _activeLoads;
private string _currentRoomId;
private readonly CompositeDisposable _subscriptions = new();
// ── IRoomStreamingManager ─────────────────────────────────────────────────
public string CurrentRoomId => _currentRoomId;
public void RegisterRoomController(RoomController controller)
{
if (controller == null) return;
if (_handles.TryGetValue(controller.RoomId, out var handle))
{
handle.RoomController = controller;
// Active 房间直接初始化相机(正常过渡流程,非后台预加载)
if (handle.State == RoomState.Active)
controller.SetupCamera();
}
else
{
// 首次注册(通常是 SceneLoader 直接加载的初始房间,绕过了流式路径)
controller.SetupCamera();
// 冷启动引导:若流式系统尚无当前房间,以此房间为起点开始预加载邻居。
// 此路径仅在游戏第一帧或从存档热载入时触发一次。
if (string.IsNullOrEmpty(_currentRoomId) && _graph != null)
{
_currentRoomId = controller.RoomId;
RecalculateStreamingSet(controller.RoomId);
Debug.Log($"[RoomStreamingManager] 冷启动:以 {controller.RoomId} 为基准启动流式预加载。");
}
}
}
///
/// 立即将指定房间加入预加载队列。
/// 用于快速传送预热、玩家即将触碰的门口等主动预加载场景。
/// 若已加载或已在队列中则忽略。
///
public void PreloadRoom(string roomId)
{
if (_handles.ContainsKey(roomId)) return;
EnqueuePreload(roomId);
}
public RoomState GetRoomState(string roomId)
{
return _handles.TryGetValue(roomId, out var handle)
? handle.State
: RoomState.Unloaded;
}
// ── ISceneLoadCoordinator ─────────────────────────────────────────────────
public bool OwnsScene(string sceneName)
=> !string.IsNullOrEmpty(sceneName) &&
sceneName.StartsWith("Room_", System.StringComparison.Ordinal);
public IEnumerator LoadAndActivateCoroutine(
string sceneName, string entryTransitionId, bool isRespawn)
{
var ctx = new SpawnContext(entryTransitionId, isRespawn);
yield return LoadAndActivateRoomCoroutine(sceneName, ctx);
}
// ── 生命周期 ──────────────────────────────────────────────────────────────
private void Awake()
{
ServiceLocator.Register(this);
ServiceLocator.Register(this);
_graph = WorldGraph.Build(_mapDatabase, _unitsPerGrid);
}
private void OnEnable()
{
_onRoomEntered?.Subscribe(OnRoomEntered).AddTo(_subscriptions);
}
private void OnDisable()
{
_subscriptions.Clear();
}
private void OnDestroy()
{
ServiceLocator.Unregister(this);
ServiceLocator.Unregister(this);
}
private void Update()
{
DrainLoadQueue();
}
// ── 房间进入事件 ───────────────────────────────────────────────────────────
private void OnRoomEntered(string roomId)
{
if (roomId == _currentRoomId) return;
_currentRoomId = roomId;
RecalculateStreamingSet(roomId);
}
// ── StreamingSet 计算 ──────────────────────────────────────────────────────
///
/// 根据当前房间重新计算需要保持 Dormant 的邻居集合,
/// 并调度加载和卸载操作。单次 BFS 完成所有计算,结果缓存供 LRU 排序复用。
///
private void RecalculateStreamingSet(string currentRoomId)
{
if (_graph == null) return;
int hops = _budget != null ? _budget.PreloadLookaheadHops : 2;
// 单次 BFS:同时服务于 keepIds 构建、预加载候选、LRU 排序三个用途
_hopCache = _graph.GetAllHopDistances(currentRoomId);
// 从跳数缓存派生保留集(跳数 ≤ hops 的所有房间 + 当前房间)
var keepIds = new HashSet { currentRoomId };
foreach (var kv in _hopCache)
{
if (kv.Value <= hops)
keepIds.Add(kv.Key);
}
// 不在保留集内的 Dormant 房间 → 开始冷却(防重复:_roomsInCooling 守卫)
foreach (var handle in _handles.Values)
{
if (!keepIds.Contains(handle.RoomId) &&
handle.State == RoomState.Dormant &&
!_roomsInCooling.Contains(handle.RoomId))
{
StartCoroutine(CoolingCoroutine(handle));
}
}
// 重建加载队列:清除废弃条目(keepIds 已缩小时不再需要的房间),
// 按跳数升序重新入队(近邻优先),确保有限并发槽优先服务最近的房间。
// 跳数 = 0 为当前房间本身,已在 Active 路径中处理,跳过以避免冷启动时重复加载。
_loadQueue.Clear();
_queuedRoomIds.Clear();
foreach (var kv in _hopCache.OrderBy(kv => kv.Value))
{
if (kv.Value == 0 || kv.Value > hops) continue;
if (!_handles.ContainsKey(kv.Key))
EnqueuePreload(kv.Key);
}
EnforceMemoryBudget();
}
// ── 预加载队列 ────────────────────────────────────────────────────────────
private void EnqueuePreload(string roomId)
{
if (_queuedRoomIds.Contains(roomId)) return; // O(1) 查重
if (_handles.ContainsKey(roomId)) return;
_loadQueue.Enqueue(roomId);
_queuedRoomIds.Add(roomId);
}
/// 每帧尝试从队列中启动新的加载,受 MaxConcurrentLoads 限制。
private void DrainLoadQueue()
{
if (_budget == null) return;
while (_activeLoads < _budget.MaxConcurrentLoads && _loadQueue.Count > 0)
{
string roomId = _loadQueue.Dequeue();
_queuedRoomIds.Remove(roomId); // 同步镜像
if (_handles.ContainsKey(roomId)) continue; // 已被其他路径加载
var node = _graph?.GetNode(roomId);
if (node == null) continue;
StartCoroutine(PreloadRoomCoroutine(roomId));
}
}
private IEnumerator PreloadRoomCoroutine(string roomId)
{
_activeLoads++;
int perFrame = _budget != null ? _budget.LifecycleActivatePerFrame : 8;
var handle = new RoomHandle(roomId, this, perFrame);
// 从图节点读取内存估算(关卡设计师在 MapRoomDataSO 中填写)
handle.EstimatedMemKB = _graph.GetNode(roomId)?.EstimatedMemoryKB ?? 0;
if (handle.EstimatedMemKB == 0)
Debug.LogWarning($"[RoomStreamingManager] {roomId}: EstimatedMemoryKB 未设置(= 0),内存预算检查将跳过此房间。" +
"请在 MapRoomDataSO 中填写 Profiler 测量值。");
_handles[roomId] = handle;
// RoomId 与 Addressable key 相同(规范:Room_{Region}_{Id})
yield return handle.LoadAsync(roomId);
_activeLoads--;
if (handle.State == RoomState.Dormant)
{
_onRoomPreloaded?.Raise(roomId);
Debug.Log($"[RoomStreamingManager] 预加载完成:{roomId}");
}
else
{
// 加载失败,从字典移除
_handles.Remove(roomId);
}
}
// ── 内存预算 / LRU 卸载 ───────────────────────────────────────────────────
private void EnforceMemoryBudget()
{
if (_budget == null) return;
// 按距离降序 + LRU 时间升序排列所有 Dormant 房间。
// 排除已在冷却协程中的房间:它们已被调度为卸载,无需重复发起 UnloadRoomCoroutine。
var dormants = _handles.Values
.Where(h => h.State == RoomState.Dormant && !_roomsInCooling.Contains(h.RoomId))
.OrderByDescending(h => _hopCache.TryGetValue(h.RoomId, out int d) ? d : int.MaxValue)
.ThenBy(h => h.LastActiveTime)
.ToList();
// 第一道:数量超限,从列表头部(距离最远/最久未访问)开始卸载
int excess = Mathf.Max(0, dormants.Count - _budget.MaxDormantRooms);
for (int i = 0; i < excess; i++)
StartCoroutine(UnloadRoomCoroutine(dormants[i].RoomId));
// 第二道:内存超限,从数量截止处继续(避免与第一道循环重复调度同一批房间)
int totalKB = _handles.Values.Sum(h => h.EstimatedMemKB);
int budgetKB = _budget.MaxMemoryMB * 1024;
if (totalKB <= budgetKB) return;
for (int i = excess; i < dormants.Count; i++)
{
if (totalKB <= budgetKB) break;
var h = dormants[i];
StartCoroutine(UnloadRoomCoroutine(h.RoomId));
totalKB -= h.EstimatedMemKB;
}
}
// ── 冷却 / 卸载 ───────────────────────────────────────────────────────────
private IEnumerator CoolingCoroutine(RoomHandle handle)
{
// 防止同一房间同时运行多个 CoolingCoroutine
if (!_roomsInCooling.Add(handle.RoomId)) yield break;
try
{
float duration = _budget != null ? _budget.CoolingDuration : 6f;
yield return new WaitForSeconds(duration);
// 冷却结束后再次检查是否仍需保留
if (_currentRoomId != null)
{
int hops = _budget != null ? _budget.PreloadLookaheadHops : 2;
// 直接复用 _hopCache(已在最近一次 RecalculateStreamingSet 中更新),避免重复 BFS
bool stillNeeded = handle.RoomId == _currentRoomId
|| (_hopCache.TryGetValue(handle.RoomId, out int dist) && dist <= hops);
if (stillNeeded)
{
// 玩家已回到本房间附近:重置为 Dormant,可再次被激活
handle.ResetToDormant();
yield break;
}
}
StartCoroutine(UnloadRoomCoroutine(handle.RoomId));
}
finally
{
_roomsInCooling.Remove(handle.RoomId);
}
}
private IEnumerator UnloadRoomCoroutine(string roomId)
{
if (!_handles.TryGetValue(roomId, out var handle)) yield break;
yield return handle.UnloadAsync();
_handles.Remove(roomId);
Debug.Log($"[RoomStreamingManager] 已卸载:{roomId}");
}
// ── TransitionDirector 调用接口 ────────────────────────────────────────────
///
/// 查询目标房间是否已就绪,可执行无等待的切换。
/// Dormant 和 Cooling 均视为就绪:Cooling 房间已完成休眠化,
/// 可立即重置为 Dormant 后激活(常见于玩家折返场景)。
///
public bool IsRoomDormant(string roomId)
=> _handles.TryGetValue(roomId, out var h) &&
(h.State == RoomState.Dormant || h.State == RoomState.Cooling);
///
/// 激活目标房间(Dormant → Active)并停用前一个房间。
/// 由 在过渡时调用。
///
public IEnumerator ActivateRoomCoroutine(string targetRoomId, SpawnContext context)
{
if (!_handles.TryGetValue(targetRoomId, out var targetHandle))
{
Debug.LogError($"[RoomStreamingManager] 目标房间 {targetRoomId} 不在已加载集合中,无法激活。");
yield break;
}
string previousRoomId = _currentRoomId;
// Cooling 房间已完成休眠化,可直接重置为 Dormant 后激活
// 场景:玩家 A→B 后立即 B→A(折返),A 处于 Cooling 状态
// CoolingCoroutine 超时后检查 _currentRoomId==A → stillNeeded=true → ResetToDormant()=无操作(已不在 Cooling),正常结束
if (targetHandle.State == RoomState.Cooling)
targetHandle.ResetToDormant();
// 激活新房间
yield return targetHandle.Activate(context);
_currentRoomId = targetRoomId;
// 旧房间进入冷却(不立即卸载;守卫防止 CoolingCoroutine 重复启动)
// 同时包含 Activating:玩家在激活过程中快速折返时也应正确触发冷却
if (!string.IsNullOrEmpty(previousRoomId) &&
_handles.TryGetValue(previousRoomId, out var prevHandle) &&
(prevHandle.State == RoomState.Active || prevHandle.State == RoomState.Activating) &&
!_roomsInCooling.Contains(previousRoomId))
{
prevHandle.BeginCooling();
StartCoroutine(CoolingCoroutine(prevHandle));
}
// 重新计算新房间的 StreamingSet
RecalculateStreamingSet(targetRoomId);
// 通知外部系统(地图探索、任务系统等):新房间已激活
_onRoomActivated?.Raise(targetRoomId);
}
///
/// 将目标房间立即加载并激活(用于 Room/Scene 类型的快速传送、复活等非预加载路径)。
/// 完成后经由 ActivateRoomCoroutine 发布 EVT_RoomActivated 并重新计算 StreamingSet。
///
public IEnumerator LoadAndActivateRoomCoroutine(string roomId, SpawnContext context)
{
if (!_handles.ContainsKey(roomId))
{
int perFrame = _budget != null ? _budget.LifecycleActivatePerFrame : 8;
var handle = new RoomHandle(roomId, this, perFrame);
handle.EstimatedMemKB = _graph?.GetNode(roomId)?.EstimatedMemoryKB ?? 0;
_handles[roomId] = handle;
yield return handle.LoadAsync(roomId);
if (handle.State != RoomState.Dormant)
{
// 加载失败:清理僵尸 handle,不继续激活(前一个房间保持 Active)
_handles.Remove(roomId);
Debug.LogError($"[RoomStreamingManager] LoadAndActivate:{roomId} 加载失败,取消激活。");
yield break;
}
}
yield return ActivateRoomCoroutine(roomId, context);
}
// ── Gizmos ────────────────────────────────────────────────────────────────
#if UNITY_EDITOR
private void OnDrawGizmos()
{
if (!Application.isPlaying || _graph == null) return;
foreach (var kv in _handles)
{
var node = _graph.GetNode(kv.Key);
if (node == null) continue;
Gizmos.color = kv.Value.State switch
{
RoomState.Active => new Color(0f, 1f, 0.3f, 0.3f),
RoomState.Dormant => new Color(0.2f, 0.6f, 1f, 0.2f),
RoomState.Loading => new Color(1f, 1f, 0f, 0.25f),
RoomState.Cooling => new Color(1f, 0.5f, 0f, 0.2f),
_ => new Color(0.5f, 0.5f, 0.5f, 0.1f),
};
Gizmos.DrawCube(node.WorldBounds.center, node.WorldBounds.size);
}
}
#endif
}
}