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 { [Header("配置")] [SerializeField] private MapDatabaseSO _mapDatabase; [SerializeField] private StreamingBudgetConfigSO _budget; [Header("事件频道 - 监听")] [Tooltip("玩家进入新房间时发布(携带 RoomId 字符串)。")] [SerializeField] private StringEventChannelSO _onRoomEntered; [Header("事件频道 - 发布")] [Tooltip("某个房间加载并进入 Dormant 状态后发布(携带 RoomId)。\n" + "供 TransitionDirector 检查是否可执行 Seamless 切换。")] [SerializeField] private StringEventChannelSO _onRoomPreloaded; [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 { // 首次进入(非流式路径也可能触发),直接初始化相机 controller.SetupCamera(); } } /// /// 立即将指定房间加入预加载队列。 /// 用于快速传送预热、玩家即将触碰的门口等主动预加载场景。 /// 若已加载或已在队列中则忽略。 /// public void PreloadRoom(string roomId) { if (_handles.ContainsKey(roomId)) return; EnqueuePreload(roomId); } // ── 生命周期 ────────────────────────────────────────────────────────────── private void Awake() { 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); } 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)); } } // 保留集内尚未加载的房间 → 加入预加载队列 foreach (var kv in _hopCache) { if (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; _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 房间 var dormants = _handles.Values .Where(h => h.State == RoomState.Dormant) .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; var neighbors = _graph.GetNeighborsWithinHops(_currentRoomId, hops); bool stillNeeded = neighbors.Any(n => n.RoomId == handle.RoomId) || handle.RoomId == _currentRoomId; 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 状态,可执行无等待的切换。 /// public bool IsRoomDormant(string roomId) => _handles.TryGetValue(roomId, out var h) && h.State == RoomState.Dormant; /// /// 激活目标房间(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; // 激活新房间 yield return targetHandle.Activate(context); _currentRoomId = targetRoomId; // 旧房间进入冷却(不立即卸载;守卫防止 CoolingCoroutine 重复启动) if (!string.IsNullOrEmpty(previousRoomId) && _handles.TryGetValue(previousRoomId, out var prevHandle) && prevHandle.State == RoomState.Active && !_roomsInCooling.Contains(previousRoomId)) { prevHandle.BeginCooling(); StartCoroutine(CoolingCoroutine(prevHandle)); } // 重新计算新房间的 StreamingSet RecalculateStreamingSet(targetRoomId); // 通知:视同进入新房间(触发地图探索更新等) _onRoomEntered?.Raise(targetRoomId); } /// /// 将目标房间立即加载并激活(用于 Scene 类型的非流式路径首次进入某房间)。 /// 完成后广播 EVT_RoomEntered 以触发 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); } 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 } }