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:
@@ -57,6 +57,7 @@ namespace BaseGames.Editor
|
||||
("SPL_", "Config"), // 法术配置 SO
|
||||
("ABL_", "Config"), // 能力配置 SO
|
||||
("MAP_", "Config"), // 地图数据 SO(AssetFolderSpec §4)
|
||||
("STR_", "Config"), // 流式加载配置 SO(StreamingBudgetConfigSO)
|
||||
("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 }), // 流式加载配置 SO(StreamingBudgetConfigSO)
|
||||
("Config/", new[] { AddressKeys.Labels.Config }),
|
||||
// ── 技能 / 法术 / 能力 / 世界物件 / 持久化:无批量加载需求,不加 Label ──
|
||||
("SKL_", Array.Empty<string>()),
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"BaseGames.Parry",
|
||||
"BaseGames.Skills",
|
||||
"BaseGames.World.Map",
|
||||
"BaseGames.World.Streaming",
|
||||
"BaseGames.EventChain",
|
||||
"BaseGames.VFX",
|
||||
"Unity.InputSystem"
|
||||
|
||||
@@ -76,6 +76,7 @@ namespace BaseGames.Editor
|
||||
_modules.Add(new FormModule());
|
||||
_modules.Add(new BossSkillModule());
|
||||
_modules.Add(new CharmModule());
|
||||
_modules.Add(new StreamingModule());
|
||||
}
|
||||
|
||||
// ── 布局 ─────────────────────────────────────────────────────────────
|
||||
|
||||
155
Assets/_Game/Scripts/Editor/Modules/StreamingModule.cs
Normal file
155
Assets/_Game/Scripts/Editor/Modules/StreamingModule.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user