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

@@ -57,6 +57,7 @@ namespace BaseGames.Editor
("SPL_", "Config"), // 法术配置 SO
("ABL_", "Config"), // 能力配置 SO
("MAP_", "Config"), // 地图数据 SOAssetFolderSpec §4
("STR_", "Config"), // 流式加载配置 SOStreamingBudgetConfigSO
("Config/", "Config"), // 路径前缀配置AssetFolderSpec §8.2
// ── 音频AUD_BGM_ / AUD_SFX_ 必须在通配 AUD_ 之前)─────────────
("AUD_BGM_", "Audio_Music"), // BGM 流式音频
@@ -79,6 +80,8 @@ namespace BaseGames.Editor
{ AddressKeys.PrefabUIFloatingDmgText, new[] { AddressKeys.Labels.Poolable, AddressKeys.Labels.Preload } },
// FootstepCatalog 是首帧必须可用的配置
{ AddressKeys.DataFootstepCatalog, new[] { AddressKeys.Labels.Config, AddressKeys.Labels.Preload } },
// 流式加载预算配置,运行时初始化前必须可用
{ AddressKeys.DataStreamingBudgetConfig, new[] { AddressKeys.Labels.Config, AddressKeys.Labels.Preload } },
};
// ── 前缀 → 标签列表 ─────────────────────────────────────────────────────
@@ -105,6 +108,7 @@ namespace BaseGames.Editor
// ── 配置数据 ─────────────────────────────────────────────────────
("CHM_", new[] { AddressKeys.Labels.Charms }),
("MAP_", new[] { AddressKeys.Labels.Config }), // 地图数据 SO 为动态加载配置
("STR_", new[] { AddressKeys.Labels.Config }), // 流式加载配置 SOStreamingBudgetConfigSO
("Config/", new[] { AddressKeys.Labels.Config }),
// ── 技能 / 法术 / 能力 / 世界物件 / 持久化:无批量加载需求,不加 Label ──
("SKL_", Array.Empty<string>()),

View File

@@ -30,6 +30,7 @@
"BaseGames.Parry",
"BaseGames.Skills",
"BaseGames.World.Map",
"BaseGames.World.Streaming",
"BaseGames.EventChain",
"BaseGames.VFX",
"Unity.InputSystem"

View File

@@ -76,6 +76,7 @@ namespace BaseGames.Editor
_modules.Add(new FormModule());
_modules.Add(new BossSkillModule());
_modules.Add(new CharmModule());
_modules.Add(new StreamingModule());
}
// ── 布局 ─────────────────────────────────────────────────────────────

View File

@@ -0,0 +1,155 @@
using System;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
using BaseGames.World.Streaming;
namespace BaseGames.Editor.Modules
{
/// <summary>
/// DataHub 流式加载模块 —— 管理 <see cref="StreamingBudgetConfigSO"/> 资产。
/// </summary>
public class StreamingModule : IDataModule
{
private const string Folder = "Assets/_Game/Data/Streaming";
private const string Prefix = "STR_";
public string ModuleId => "streaming";
public string DisplayName => "流式加载";
public string IconName => "d_RectTransformBlueprint";
private SoListPane<StreamingBudgetConfigSO> _listPane;
private DetailHeader _header;
private StreamingBudgetConfigSO _selected;
public void Initialize()
{
_listPane = new SoListPane<StreamingBudgetConfigSO>(
Folder, Prefix,
cfg => $"休眠上限 {cfg.MaxDormantRooms} 预加载深度 {cfg.PreloadLookaheadHops}跳");
_listPane.SelectionChanged = sel =>
{
_selected = sel;
};
}
public void BuildListPane(VisualElement container, Action<UnityEngine.Object> onSelected)
{
_listPane.SelectionChanged = sel =>
{
_selected = sel;
onSelected?.Invoke(sel);
};
// 顶部操作栏(新建)
var topBar = new VisualElement();
topBar.style.flexDirection = FlexDirection.Row;
topBar.style.paddingLeft = 8;
topBar.style.paddingRight = 8;
topBar.style.paddingTop = 6;
topBar.style.paddingBottom = 6;
topBar.style.borderBottomWidth = 1;
topBar.style.borderBottomColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.3f));
container.Add(topBar);
var createBtn = new Button(() =>
{
var created = AssetOperations.Create<StreamingBudgetConfigSO>(Folder, "STR_BudgetConfig_New");
if (created != null) _listPane.Refresh(created);
}) { text = "+ 新建配置" };
createBtn.style.flexGrow = 1;
topBar.Add(createBtn);
container.Add(_listPane);
_listPane.style.flexGrow = 1;
_listPane.Refresh();
}
public void BuildDetailPane(VisualElement container, UnityEngine.Object selected)
{
_selected = selected as StreamingBudgetConfigSO;
_header = new DetailHeader();
_header.SetAsset(_selected);
_header.RenameRequested += newName => OnRenameRequested(selected, newName);
container.Add(_header);
if (_selected == null) return;
container.Add(BuildStatsCard(_selected));
container.Add(BuildActionBar(_selected));
container.Add(SkillModule.MakeDivider());
container.Add(new InspectorElement(_selected));
}
public void OnActivated() => _listPane?.Refresh();
// ── Stats Card ────────────────────────────────────────────────────────
private static VisualElement BuildStatsCard(StreamingBudgetConfigSO cfg)
{
var card = SkillModule.MakeCard();
SkillModule.AddChip(card, "最大休眠房间", $"{cfg.MaxDormantRooms}");
SkillModule.AddChip(card, "内存上限", $"{cfg.MaxMemoryMB} MB");
SkillModule.AddChip(card, "并发加载数", $"{cfg.MaxConcurrentLoads}");
SkillModule.AddChip(card, "预加载跳数", $"{cfg.PreloadLookaheadHops}");
SkillModule.AddChip(card, "冷却时长", $"{cfg.CoolingDuration:F1}s");
SkillModule.AddChip(card, "每帧激活数", $"{cfg.LifecycleActivatePerFrame}");
return card;
}
// ── Action Bar ────────────────────────────────────────────────────────
private VisualElement BuildActionBar(StreamingBudgetConfigSO cfg)
{
var bar = SkillModule.MakeActionBar();
new Button(() =>
{
EditorGUIUtility.PingObject(cfg);
Selection.activeObject = cfg;
}) { text = "定位" }.AlsoAddTo(bar);
new Button(() =>
{
var c = AssetOperations.Clone(cfg, Folder);
if (c != null) _listPane.Refresh(c);
}) { text = "克隆..." }.AlsoAddTo(bar);
var del = new Button(() =>
{
if (AssetOperations.Delete(cfg)) _listPane.Refresh(null);
}) { text = "删除" };
ApplyDeleteStyle(del);
del.AlsoAddTo(bar);
return bar;
}
// ── 重命名 ────────────────────────────────────────────────────────────
private void OnRenameRequested(UnityEngine.Object asset, string newName)
{
var (ok, err) = AssetOperations.Rename(asset, newName);
if (!ok) EditorUtility.DisplayDialog("重命名失败", err, "确定");
else { _header.SetAsset(asset); _listPane.Invalidate(); }
}
// ── 共用 ─────────────────────────────────────────────────────────────
private static void ApplyDeleteStyle(Button btn)
{
var c = new StyleColor(new Color(0.8f, 0.3f, 0.3f, 0.6f));
btn.style.borderLeftColor = c;
btn.style.borderRightColor = c;
btn.style.borderTopColor = c;
btn.style.borderBottomColor = c;
btn.style.borderLeftWidth = 1;
btn.style.borderRightWidth = 1;
btn.style.borderTopWidth = 1;
btn.style.borderBottomWidth = 1;
btn.style.marginLeft = 8;
}
}
}

View File

@@ -12,6 +12,8 @@ using BaseGames.UI.MainMenu;
using BaseGames.UI.Menus;
using BaseGames.UI.Splash;
using BaseGames.World;
using BaseGames.World.Map;
using BaseGames.World.Streaming;
using PathBerserker2d;
using Unity.Cinemachine;
using UnityEditor;
@@ -230,6 +232,9 @@ namespace BaseGames.Editor
AddScaffoldNote(hudRootGo, "HUDController 已挂载。其内部图片/文本/图标 Prefab 依赖较多,需后续手工补 UI 资源与事件频道。", report);
// ── 流式加载系统 ──────────────────────────────────────────────────
ScaffoldStreamingSystem(services, report);
MarkDirtyAndLog("Persistent 场景脚手架", root, report);
}
@@ -392,6 +397,79 @@ namespace BaseGames.Editor
MarkDirtyAndLog("Game Room 脚手架", root, report);
}
// ─────────────────────────────────────────────────────────────────────
// 流式加载系统RoomStreamingManager + TransitionDirector
// ─────────────────────────────────────────────────────────────────────
/// <summary>
/// 在 [Services] 下创建或更新 SYS_RoomStreamingManager
/// 挂载 <see cref="RoomStreamingManager"/> 与 <see cref="TransitionDirector"/>
/// 并自动绑定已存在的事件频道与配置资产。
/// </summary>
private static void ScaffoldStreamingSystem(Transform services, List<string> report)
{
// 预算配置 SO不存在时自动创建
StreamingBudgetConfigSO budgetConfig = EnsureStreamingBudgetConfigAsset(report);
// MapDatabaseSO查找已存在的资产
Object mapDbAsset = FindFirstAssetByType<MapDatabaseSO>("MapDatabase", "MAP_Database", "MapDatabaseSO");
if (mapDbAsset == null)
report.Add("未找到 MapDatabaseSO 资产。请将 MapDatabaseSO 手工赋给 RoomStreamingManager._mapDatabase 与 TransitionDirector._mapDatabase。");
// ── SYS_RoomStreamingManager GameObject ──────────────────────────
GameObject streamingGo = GetOrCreateChild(services, "SYS_RoomStreamingManager").gameObject;
RoomStreamingManager streamingMgr = GetOrAddComponent<RoomStreamingManager>(streamingGo);
TransitionDirector transitionDir = GetOrAddComponent<TransitionDirector>(streamingGo);
// ── RoomStreamingManager 字段 ─────────────────────────────────────
AssignReference(streamingMgr, "_mapDatabase", mapDbAsset);
AssignReference(streamingMgr, "_budget", budgetConfig);
AssignAsset(streamingMgr, "_onRoomEntered", report, false, "EVT_RoomEntered");
AssignAsset(streamingMgr, "_onRoomPreloaded", report, false, "EVT_RoomPreloaded");
// ── TransitionDirector 字段 ───────────────────────────────────────
AssignReference(transitionDir, "_streamingManager", streamingMgr);
AssignReference(transitionDir, "_mapDatabase", mapDbAsset);
AssignReference(transitionDir, "_budget", budgetConfig);
AssignAsset(transitionDir, "_onFadeOutRequest", report, false, "EVT_FadeOutRequest");
AssignAsset(transitionDir, "_onFadeInRequest", report, false, "EVT_FadeInRequest");
AssignAsset(transitionDir, "_onRegionNameDisplay", report, false, "EVT_RegionNameDisplay");
AssignAsset(transitionDir, "_onSceneWorldStateRestored", report, false, "EVT_SceneWorldStateRestored");
report.Add("SYS_RoomStreamingManager流式加载系统已创建。如 EVT_RoomEntered / EVT_RoomPreloaded 频道尚未存在,请通过 DataHub > Streaming 创建后重新运行脚手架。");
}
/// <summary>
/// 在 <c>Assets/_Game/Data/Streaming/</c> 下确保默认预算配置 SO 存在。
/// 已存在时直接返回;不存在时自动创建 <c>STR_BudgetConfig_Default.asset</c>。
/// </summary>
private static StreamingBudgetConfigSO EnsureStreamingBudgetConfigAsset(List<string> report)
{
// 先查找已有资产
string[] guids = AssetDatabase.FindAssets("t:StreamingBudgetConfigSO");
if (guids != null && guids.Length > 0)
{
string path = AssetDatabase.GUIDToAssetPath(guids[0]);
var existing = AssetDatabase.LoadAssetAtPath<StreamingBudgetConfigSO>(path);
if (existing != null)
return existing;
}
// 没有则创建默认资产
const string folder = "Assets/_Game/Data/Streaming";
const string assetPath = folder + "/STR_BudgetConfig_Default.asset";
EnsureFolder(folder);
var created = ScriptableObject.CreateInstance<StreamingBudgetConfigSO>();
AssetDatabase.CreateAsset(created, assetPath);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
report.Add($"已自动创建流式加载预算配置:{assetPath}。可在 DataHub > 流式加载 中编辑默认参数。");
return created;
}
private static void AssignString(Object target, string propertyName, string value, List<string> report = null)
{
SerializedObject serializedObject = new SerializedObject(target);