- Implemented EnemyRespawner to manage enemy spawning and respawning within rooms. - Added IRoomLifecycle interface for room activation and dormancy handling. - Created supporting classes and metadata for enemy perception and threat assessment. - Established streaming system components for room state management and transitions. - Added necessary metadata files for new scripts to ensure proper integration with Unity.
453 lines
21 KiB
C#
453 lines
21 KiB
C#
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
|
||
{
|
||
/// <summary>
|
||
/// 核心流式加载调度器。挂载于 Persistent 场景的 SYS_RoomStreamingManager 对象上。
|
||
/// <para>
|
||
/// 职责:
|
||
/// <list type="bullet">
|
||
/// <item>在 Persistent 场景初始化时从 MapDatabaseSO 构建 <see cref="WorldGraph"/></item>
|
||
/// <item>监听 <see cref="EVT_RoomEntered"/> 事件,重新计算 StreamingSet</item>
|
||
/// <item>后台异步预加载邻居房间(<see cref="RoomState.Dormant"/>),并发数由预算配置限制</item>
|
||
/// <item>执行内存预算检查,通过 LRU 策略卸载距离最远、最久未访问的 Dormant 房间</item>
|
||
/// <item>提供 ActivateRoom / DeactivateRoom 接口供 <see cref="TransitionDirector"/> 调用</item>
|
||
/// </list>
|
||
/// </para>
|
||
/// </summary>
|
||
[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<string, RoomHandle> _handles = new();
|
||
private readonly Queue<string> _loadQueue = new();
|
||
/// <summary>O(1) Contains 镜像,与 _loadQueue 保持同步,避免 Queue.Contains 的 O(n) 开销。</summary>
|
||
private readonly HashSet<string> _queuedRoomIds = new();
|
||
/// <summary>当前正在运行 CoolingCoroutine 的房间,防止竞态下重复启动冷却协程。</summary>
|
||
private readonly HashSet<string> _roomsInCooling = new();
|
||
/// <summary>单次 BFS 预算:从当前房间到所有已加载房间的跳数缓存,供 LRU 排序使用。</summary>
|
||
private Dictionary<string, int> _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} 为基准启动流式预加载。");
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 立即将指定房间加入预加载队列。
|
||
/// 用于快速传送预热、玩家即将触碰的门口等主动预加载场景。
|
||
/// 若已加载或已在队列中则忽略。
|
||
/// </summary>
|
||
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<IRoomStreamingManager>(this);
|
||
ServiceLocator.Register<ISceneLoadCoordinator>(this);
|
||
_graph = WorldGraph.Build(_mapDatabase, _unitsPerGrid);
|
||
}
|
||
|
||
private void OnEnable()
|
||
{
|
||
_onRoomEntered?.Subscribe(OnRoomEntered).AddTo(_subscriptions);
|
||
}
|
||
|
||
private void OnDisable()
|
||
{
|
||
_subscriptions.Clear();
|
||
}
|
||
|
||
private void OnDestroy()
|
||
{
|
||
ServiceLocator.Unregister<IRoomStreamingManager>(this);
|
||
ServiceLocator.Unregister<ISceneLoadCoordinator>(this);
|
||
}
|
||
|
||
private void Update()
|
||
{
|
||
DrainLoadQueue();
|
||
}
|
||
|
||
// ── 房间进入事件 ───────────────────────────────────────────────────────────
|
||
|
||
private void OnRoomEntered(string roomId)
|
||
{
|
||
if (roomId == _currentRoomId) return;
|
||
_currentRoomId = roomId;
|
||
RecalculateStreamingSet(roomId);
|
||
}
|
||
|
||
// ── StreamingSet 计算 ──────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 根据当前房间重新计算需要保持 Dormant 的邻居集合,
|
||
/// 并调度加载和卸载操作。单次 BFS 完成所有计算,结果缓存供 LRU 排序复用。
|
||
/// </summary>
|
||
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<string> { 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);
|
||
}
|
||
|
||
/// <summary>每帧尝试从队列中启动新的加载,受 MaxConcurrentLoads 限制。</summary>
|
||
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 调用接口 ────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 查询目标房间是否已就绪,可执行无等待的切换。
|
||
/// Dormant 和 Cooling 均视为就绪:Cooling 房间已完成休眠化,
|
||
/// 可立即重置为 Dormant 后激活(常见于玩家折返场景)。
|
||
/// </summary>
|
||
public bool IsRoomDormant(string roomId)
|
||
=> _handles.TryGetValue(roomId, out var h) &&
|
||
(h.State == RoomState.Dormant || h.State == RoomState.Cooling);
|
||
|
||
/// <summary>
|
||
/// 激活目标房间(Dormant → Active)并停用前一个房间。
|
||
/// 由 <see cref="TransitionDirector"/> 在过渡时调用。
|
||
/// </summary>
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将目标房间立即加载并激活(用于 Room/Scene 类型的快速传送、复活等非预加载路径)。
|
||
/// 完成后经由 ActivateRoomCoroutine 发布 EVT_RoomActivated 并重新计算 StreamingSet。
|
||
/// </summary>
|
||
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
|
||
}
|
||
}
|