Add enemy respawner and related components for room lifecycle management
- 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.
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 18b849fabd3ac314bbe36055acb3a4b8
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -96,6 +96,13 @@ namespace BaseGames.World.Streaming
|
||||
{
|
||||
if (State == RoomState.Unloaded || State == RoomState.Unloading || !_sceneHandle.IsValid()) yield break;
|
||||
|
||||
// 若正处于加载中,等待加载完成后再卸载(Addressables 不支持对未完成的句柄直接卸载)
|
||||
if (State == RoomState.Loading)
|
||||
{
|
||||
while (!_sceneHandle.IsDone)
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Active 状态先走 Deactivate,再设置 Unloading
|
||||
bool needsDeactivate = State == RoomState.Active ||
|
||||
State == RoomState.Activating ||
|
||||
|
||||
11
Assets/_Game/Scripts/World/Streaming/RoomHandle.cs.meta
Normal file
11
Assets/_Game/Scripts/World/Streaming/RoomHandle.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c41dde1869912a647a425338b185282f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -22,14 +22,16 @@ namespace BaseGames.World.Streaming
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[DefaultExecutionOrder(-800)]
|
||||
public class RoomStreamingManager : MonoBehaviour, IRoomStreamingManager
|
||||
public class RoomStreamingManager : MonoBehaviour, IRoomStreamingManager, ISceneLoadCoordinator
|
||||
{
|
||||
[Header("配置")]
|
||||
[SerializeField] private MapDatabaseSO _mapDatabase;
|
||||
[SerializeField] private StreamingBudgetConfigSO _budget;
|
||||
|
||||
[Header("事件频道 - 监听")]
|
||||
[Tooltip("玩家进入新房间时发布(携带 RoomId 字符串)。")]
|
||||
[Header("事件频道 - 监听(输入)")]
|
||||
[Tooltip("外部触发器或非流式加载路径在玩家到达新房间时发布此事件(携带 RoomId)。\n" +
|
||||
"流式管理器订阅此频道以支持冷启动和外部调试传送。\n" +
|
||||
"正常流式过渡路径由 ActivateRoomCoroutine 直接更新 _currentRoomId,不依赖此订阅。")]
|
||||
[SerializeField] private StringEventChannelSO _onRoomEntered;
|
||||
|
||||
[Header("事件频道 - 发布")]
|
||||
@@ -37,6 +39,11 @@ namespace BaseGames.World.Streaming
|
||||
"供 TransitionDirector 检查是否可执行 Seamless 切换。")]
|
||||
[SerializeField] private StringEventChannelSO _onRoomPreloaded;
|
||||
|
||||
[Tooltip("房间激活完成(Active)后发布(携带 RoomId)。\n" +
|
||||
"地图探索、任务系统等应订阅此事件以感知当前房间变更。\n" +
|
||||
"注意:与 _onRoomEntered(输入)是不同频道,避免流式管理器自身产生事件循环。")]
|
||||
[SerializeField] private StringEventChannelSO _onRoomActivated;
|
||||
|
||||
[Header("格子单位(世界坐标)")]
|
||||
[Tooltip("每个格子对应的 Unity 世界坐标单位数,与关卡设计网格对齐。")]
|
||||
[SerializeField] private float _unitsPerGrid = 16f;
|
||||
@@ -73,8 +80,17 @@ namespace BaseGames.World.Streaming
|
||||
}
|
||||
else
|
||||
{
|
||||
// 首次进入(非流式路径也可能触发),直接初始化相机
|
||||
// 首次注册(通常是 SceneLoader 直接加载的初始房间,绕过了流式路径)
|
||||
controller.SetupCamera();
|
||||
|
||||
// 冷启动引导:若流式系统尚无当前房间,以此房间为起点开始预加载邻居。
|
||||
// 此路径仅在游戏第一帧或从存档热载入时触发一次。
|
||||
if (string.IsNullOrEmpty(_currentRoomId) && _graph != null)
|
||||
{
|
||||
_currentRoomId = controller.RoomId;
|
||||
RecalculateStreamingSet(controller.RoomId);
|
||||
Debug.Log($"[RoomStreamingManager] 冷启动:以 {controller.RoomId} 为基准启动流式预加载。");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,11 +105,32 @@ namespace BaseGames.World.Streaming
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -110,6 +147,7 @@ namespace BaseGames.World.Streaming
|
||||
private void OnDestroy()
|
||||
{
|
||||
ServiceLocator.Unregister<IRoomStreamingManager>(this);
|
||||
ServiceLocator.Unregister<ISceneLoadCoordinator>(this);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
@@ -160,10 +198,14 @@ namespace BaseGames.World.Streaming
|
||||
}
|
||||
}
|
||||
|
||||
// 保留集内尚未加载的房间 → 加入预加载队列
|
||||
foreach (var kv in _hopCache)
|
||||
// 重建加载队列:清除废弃条目(keepIds 已缩小时不再需要的房间),
|
||||
// 按跳数升序重新入队(近邻优先),确保有限并发槽优先服务最近的房间。
|
||||
// 跳数 = 0 为当前房间本身,已在 Active 路径中处理,跳过以避免冷启动时重复加载。
|
||||
_loadQueue.Clear();
|
||||
_queuedRoomIds.Clear();
|
||||
foreach (var kv in _hopCache.OrderBy(kv => kv.Value))
|
||||
{
|
||||
if (kv.Value > hops) continue;
|
||||
if (kv.Value == 0 || kv.Value > hops) continue;
|
||||
if (!_handles.ContainsKey(kv.Key))
|
||||
EnqueuePreload(kv.Key);
|
||||
}
|
||||
@@ -206,6 +248,9 @@ namespace BaseGames.World.Streaming
|
||||
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})
|
||||
@@ -231,9 +276,10 @@ namespace BaseGames.World.Streaming
|
||||
{
|
||||
if (_budget == null) return;
|
||||
|
||||
// 按距离降序 + LRU 时间升序排列所有 Dormant 房间
|
||||
// 按距离降序 + LRU 时间升序排列所有 Dormant 房间。
|
||||
// 排除已在冷却协程中的房间:它们已被调度为卸载,无需重复发起 UnloadRoomCoroutine。
|
||||
var dormants = _handles.Values
|
||||
.Where(h => h.State == RoomState.Dormant)
|
||||
.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();
|
||||
@@ -272,10 +318,10 @@ namespace BaseGames.World.Streaming
|
||||
// 冷却结束后再次检查是否仍需保留
|
||||
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;
|
||||
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,可再次被激活
|
||||
@@ -303,10 +349,13 @@ namespace BaseGames.World.Streaming
|
||||
// ── TransitionDirector 调用接口 ────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 查询目标房间是否已处于 Dormant 状态,可执行无等待的切换。
|
||||
/// 查询目标房间是否已就绪,可执行无等待的切换。
|
||||
/// Dormant 和 Cooling 均视为就绪:Cooling 房间已完成休眠化,
|
||||
/// 可立即重置为 Dormant 后激活(常见于玩家折返场景)。
|
||||
/// </summary>
|
||||
public bool IsRoomDormant(string roomId)
|
||||
=> _handles.TryGetValue(roomId, out var h) && h.State == RoomState.Dormant;
|
||||
=> _handles.TryGetValue(roomId, out var h) &&
|
||||
(h.State == RoomState.Dormant || h.State == RoomState.Cooling);
|
||||
|
||||
/// <summary>
|
||||
/// 激活目标房间(Dormant → Active)并停用前一个房间。
|
||||
@@ -322,14 +371,21 @@ namespace BaseGames.World.Streaming
|
||||
|
||||
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.Active || prevHandle.State == RoomState.Activating) &&
|
||||
!_roomsInCooling.Contains(previousRoomId))
|
||||
{
|
||||
prevHandle.BeginCooling();
|
||||
@@ -339,13 +395,13 @@ namespace BaseGames.World.Streaming
|
||||
// 重新计算新房间的 StreamingSet
|
||||
RecalculateStreamingSet(targetRoomId);
|
||||
|
||||
// 通知:视同进入新房间(触发地图探索更新等)
|
||||
_onRoomEntered?.Raise(targetRoomId);
|
||||
// 通知外部系统(地图探索、任务系统等):新房间已激活
|
||||
_onRoomActivated?.Raise(targetRoomId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将目标房间立即加载并激活(用于 Scene 类型的非流式路径首次进入某房间)。
|
||||
/// 完成后广播 EVT_RoomEntered 以触发 StreamingSet 重新计算。
|
||||
/// 将目标房间立即加载并激活(用于 Room/Scene 类型的快速传送、复活等非预加载路径)。
|
||||
/// 完成后经由 ActivateRoomCoroutine 发布 EVT_RoomActivated 并重新计算 StreamingSet。
|
||||
/// </summary>
|
||||
public IEnumerator LoadAndActivateRoomCoroutine(string roomId, SpawnContext context)
|
||||
{
|
||||
@@ -353,9 +409,17 @@ namespace BaseGames.World.Streaming
|
||||
{
|
||||
int perFrame = _budget != null ? _budget.LifecycleActivatePerFrame : 8;
|
||||
var handle = new RoomHandle(roomId, this, perFrame);
|
||||
handle.EstimatedMemKB = _graph.GetNode(roomId)?.EstimatedMemoryKB ?? 0;
|
||||
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);
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0c17eea5df8bd684599801eb7dd7c41f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 65e349713a81a3846a654ea9f428ef99
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a6db86de2f8c90548ba7e185b9cd5df6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -140,43 +140,6 @@ namespace BaseGames.World.Streaming
|
||||
return node;
|
||||
}
|
||||
|
||||
/// <summary>返回与指定房间直接相邻(1 跳)的所有邻居节点。</summary>
|
||||
public IEnumerable<RoomNode> GetDirectNeighbors(string roomId)
|
||||
{
|
||||
if (!_nodes.TryGetValue(roomId, out var node)) yield break;
|
||||
foreach (var edge in node.Edges)
|
||||
yield return edge.To;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 BFS 获取距离指定房间不超过 <paramref name="maxHops"/> 跳的所有房间节点集合。
|
||||
/// 结果不包含起始节点本身。
|
||||
/// </summary>
|
||||
public HashSet<RoomNode> GetNeighborsWithinHops(string startRoomId, int maxHops)
|
||||
{
|
||||
var result = new HashSet<RoomNode>();
|
||||
var visited = new HashSet<string> { startRoomId };
|
||||
var queue = new Queue<(string roomId, int depth)>();
|
||||
queue.Enqueue((startRoomId, 0));
|
||||
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var (current, depth) = queue.Dequeue();
|
||||
if (!_nodes.TryGetValue(current, out var node)) continue;
|
||||
|
||||
foreach (var edge in node.Edges)
|
||||
{
|
||||
if (visited.Contains(edge.To.RoomId)) continue;
|
||||
visited.Add(edge.To.RoomId);
|
||||
result.Add(edge.To);
|
||||
if (depth + 1 < maxHops)
|
||||
queue.Enqueue((edge.To.RoomId, depth + 1));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算两个房间之间的最短跳数(BFS)。
|
||||
/// 若不可达返回 int.MaxValue。
|
||||
|
||||
11
Assets/_Game/Scripts/World/Streaming/WorldGraph.cs.meta
Normal file
11
Assets/_Game/Scripts/World/Streaming/WorldGraph.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8e6c4da6273ca654cbf30351d31202ed
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user