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:
266
Assets/_Game/Scripts/Editor/Enemies/EnemyRespawnerEditor.cs
Normal file
266
Assets/_Game/Scripts/Editor/Enemies/EnemyRespawnerEditor.cs
Normal file
@@ -0,0 +1,266 @@
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using BaseGames.Enemies;
|
||||
using BaseGames.World;
|
||||
|
||||
namespace BaseGames.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="EnemyRespawner"/> 自定义 Inspector。
|
||||
/// <list type="bullet">
|
||||
/// <item>配置校验:生成来源为空、永久击杀 ID 已填但 WorldStateRegistry 未关联时显示警告。</item>
|
||||
/// <item>运行时状态:PlayMode 中显示当前激活实例信息。</item>
|
||||
/// <item>批量验证菜单:BaseGames / Enemies / Validate Respawners。</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
[CustomEditor(typeof(EnemyRespawner))]
|
||||
public class EnemyRespawnerEditor : UnityEditor.Editor
|
||||
{
|
||||
// ── 序列化属性引用 ─────────────────────────────────────────────────────────
|
||||
private SerializedProperty _prefabProp;
|
||||
private SerializedProperty _poolKeyProp;
|
||||
private SerializedProperty _respawnDelayProp;
|
||||
private SerializedProperty _maxRespawnCountProp;
|
||||
private SerializedProperty _persistentKillIdProp;
|
||||
private SerializedProperty _worldStateProp;
|
||||
|
||||
// 运行时只读字段(反射获取私有字段)
|
||||
private static readonly System.Reflection.FieldInfo s_ActiveEnemyField =
|
||||
typeof(EnemyRespawner).GetField("_activeEnemy",
|
||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||
|
||||
private static readonly System.Reflection.FieldInfo s_RespawnCountField =
|
||||
typeof(EnemyRespawner).GetField("_respawnCountThisVisit",
|
||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||
|
||||
private static readonly System.Reflection.FieldInfo s_RespawnCoroutineField =
|
||||
typeof(EnemyRespawner).GetField("_respawnCoroutine",
|
||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
_prefabProp = serializedObject.FindProperty("_enemyPrefab");
|
||||
_poolKeyProp = serializedObject.FindProperty("_poolKey");
|
||||
_respawnDelayProp = serializedObject.FindProperty("_respawnDelay");
|
||||
_maxRespawnCountProp = serializedObject.FindProperty("_maxRespawnCount");
|
||||
_persistentKillIdProp = serializedObject.FindProperty("_persistentKillId");
|
||||
_worldStateProp = serializedObject.FindProperty("_worldState");
|
||||
}
|
||||
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
serializedObject.Update();
|
||||
|
||||
// 手动绘制字段,以便对 _maxRespawnCount 做条件性灰显
|
||||
DrawPropertiesExcluding(serializedObject, "_maxRespawnCount");
|
||||
|
||||
bool hasDelay = _respawnDelayProp != null && _respawnDelayProp.floatValue > 0f;
|
||||
using (new EditorGUI.DisabledScope(!hasDelay))
|
||||
{
|
||||
EditorGUILayout.PropertyField(_maxRespawnCountProp,
|
||||
new GUIContent("复活上限 (Max Respawn Count)",
|
||||
_maxRespawnCountProp?.tooltip ?? ""));
|
||||
}
|
||||
if (!hasDelay)
|
||||
EditorGUILayout.HelpBox("Respawn Delay = 0,Max Respawn Count 无效(敌人不会在房间内复活)。",
|
||||
MessageType.None);
|
||||
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
|
||||
EditorGUILayout.Space(6f);
|
||||
DrawValidationSection();
|
||||
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
EditorGUILayout.Space(6f);
|
||||
DrawRuntimeSection();
|
||||
}
|
||||
}
|
||||
|
||||
// ── 配置校验区块 ──────────────────────────────────────────────────────────
|
||||
|
||||
private void DrawValidationSection()
|
||||
{
|
||||
EditorGUILayout.LabelField("配置检查", EditorStyles.boldLabel);
|
||||
|
||||
bool hasPrefab = _prefabProp.objectReferenceValue != null;
|
||||
bool hasPoolKey = !string.IsNullOrWhiteSpace(_poolKeyProp.stringValue);
|
||||
bool hasPersistentId = !string.IsNullOrWhiteSpace(_persistentKillIdProp.stringValue);
|
||||
bool hasWorldState = _worldStateProp.objectReferenceValue != null;
|
||||
|
||||
// ── 生成来源 ───────────────────────────────────────────────────────
|
||||
if (!hasPrefab && !hasPoolKey)
|
||||
{
|
||||
EditorGUILayout.HelpBox(
|
||||
"_enemyPrefab 与 _poolKey 均为空,运行时无法生成敌人。\n" +
|
||||
"请至少填写其中一项。",
|
||||
MessageType.Error);
|
||||
}
|
||||
else if (hasPrefab && !hasPoolKey)
|
||||
{
|
||||
EditorGUILayout.HelpBox(
|
||||
"当前使用直接实例化(_enemyPrefab)。\n" +
|
||||
"频繁进出房间时建议改用对象池(填写 _poolKey)以减少 GC 压力。",
|
||||
MessageType.Info);
|
||||
}
|
||||
else
|
||||
{
|
||||
DrawOk("生成来源配置正常。");
|
||||
}
|
||||
|
||||
// ── 永久击杀 ───────────────────────────────────────────────────────
|
||||
if (hasPersistentId && !hasWorldState)
|
||||
{
|
||||
EditorGUILayout.HelpBox(
|
||||
"_persistentKillId 已填写,但 _worldState 未关联。\n" +
|
||||
"击杀记录将无法持久化,存档重载后敌人仍会复活。",
|
||||
MessageType.Warning);
|
||||
|
||||
if (GUILayout.Button("自动查找场景内 WorldStateRegistry"))
|
||||
{
|
||||
var found = FindFirstObjectByType<WorldStateRegistry>();
|
||||
if (found != null)
|
||||
{
|
||||
serializedObject.Update();
|
||||
_worldStateProp.objectReferenceValue = found;
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorUtility.DisplayDialog(
|
||||
"未找到",
|
||||
"当前场景中没有 WorldStateRegistry 组件。\n" +
|
||||
"请先在场景内放置 WorldStateRegistry。",
|
||||
"确定");
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!hasPersistentId)
|
||||
{
|
||||
DrawOk("普通刷新点(每次进房复活)。");
|
||||
}
|
||||
else
|
||||
{
|
||||
DrawOk($"永久击杀已关联 WorldStateRegistry。\nID:{_persistentKillIdProp.stringValue}");
|
||||
}
|
||||
}
|
||||
|
||||
// ── 运行时状态区块(PlayMode 专用)────────────────────────────────────────
|
||||
|
||||
private void DrawRuntimeSection()
|
||||
{
|
||||
EditorGUILayout.LabelField("运行时状态", EditorStyles.boldLabel);
|
||||
|
||||
var respawner = (EnemyRespawner)target;
|
||||
var activeEnemy = s_ActiveEnemyField?.GetValue(respawner) as EnemyBase;
|
||||
int respawnCount = s_RespawnCountField != null
|
||||
? (int)s_RespawnCountField.GetValue(respawner)
|
||||
: 0;
|
||||
bool waitingRespawn = s_RespawnCoroutineField?.GetValue(respawner) != null;
|
||||
|
||||
if (activeEnemy == null && !waitingRespawn)
|
||||
{
|
||||
EditorGUILayout.LabelField("当前实例", "无(房间休眠或尚未生成)");
|
||||
}
|
||||
else if (waitingRespawn)
|
||||
{
|
||||
var prevColor = GUI.color;
|
||||
GUI.color = new Color(0.6f, 0.8f, 1f);
|
||||
int maxCount = _maxRespawnCountProp?.intValue ?? 0;
|
||||
string countStr = maxCount == 0
|
||||
? $"{respawnCount} 次(无上限)"
|
||||
: $"{respawnCount} / {maxCount} 次";
|
||||
EditorGUILayout.LabelField("状态", $"⏳ 等待复活中…(已复活 {countStr})");
|
||||
GUI.color = prevColor;
|
||||
}
|
||||
else if (activeEnemy != null)
|
||||
{
|
||||
string statusStr = activeEnemy.IsAlive ? "存活" : "死亡动画中";
|
||||
var prevColor = GUI.color;
|
||||
GUI.color = activeEnemy.IsAlive
|
||||
? new Color(0.4f, 0.9f, 0.4f)
|
||||
: new Color(1f, 0.55f, 0.3f);
|
||||
EditorGUILayout.LabelField("当前实例", $"{activeEnemy.name} [{statusStr}]");
|
||||
GUI.color = prevColor;
|
||||
|
||||
int maxCount = _maxRespawnCountProp?.intValue ?? 0;
|
||||
if (_respawnDelayProp?.floatValue > 0f)
|
||||
{
|
||||
string countStr = maxCount == 0
|
||||
? $"{respawnCount} 次(无上限)"
|
||||
: $"{respawnCount} / {maxCount} 次";
|
||||
EditorGUILayout.LabelField("本次访问已复活", countStr);
|
||||
}
|
||||
|
||||
if (GUILayout.Button("在 Hierarchy 中定位", EditorStyles.miniButton))
|
||||
Selection.activeGameObject = activeEnemy.gameObject;
|
||||
}
|
||||
|
||||
// 强制每帧刷新,确保状态即时更新
|
||||
Repaint();
|
||||
}
|
||||
|
||||
// ── 通用辅助 ──────────────────────────────────────────────────────────────
|
||||
|
||||
private static void DrawOk(string message)
|
||||
{
|
||||
var prevColor = GUI.color;
|
||||
GUI.color = new Color(0.4f, 0.9f, 0.4f);
|
||||
EditorGUILayout.LabelField($"✓ {message}", EditorStyles.wordWrappedMiniLabel);
|
||||
GUI.color = prevColor;
|
||||
}
|
||||
|
||||
// ── 批量验证菜单 ──────────────────────────────────────────────────────────
|
||||
|
||||
[MenuItem("BaseGames/Enemies/Validate Respawners")]
|
||||
private static void ValidateAllRespawners()
|
||||
{
|
||||
var all = FindObjectsByType<EnemyRespawner>(
|
||||
FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||||
|
||||
if (all.Length == 0)
|
||||
{
|
||||
EditorUtility.DisplayDialog("验证结果", "当前场景中未找到任何 EnemyRespawner。", "确定");
|
||||
return;
|
||||
}
|
||||
|
||||
int errors = 0;
|
||||
int warnings = 0;
|
||||
|
||||
foreach (var r in all)
|
||||
{
|
||||
var so = new SerializedObject(r);
|
||||
var prefab = so.FindProperty("_enemyPrefab");
|
||||
var poolKey = so.FindProperty("_poolKey");
|
||||
var killId = so.FindProperty("_persistentKillId");
|
||||
var worldState = so.FindProperty("_worldState");
|
||||
|
||||
bool hasPrefab = prefab?.objectReferenceValue != null;
|
||||
bool hasPoolKey = !string.IsNullOrWhiteSpace(poolKey?.stringValue);
|
||||
bool hasKillId = !string.IsNullOrWhiteSpace(killId?.stringValue);
|
||||
bool hasWS = worldState?.objectReferenceValue != null;
|
||||
|
||||
string scene = r.gameObject.scene.name;
|
||||
string path = $"{scene}/{r.gameObject.name}";
|
||||
|
||||
if (!hasPrefab && !hasPoolKey)
|
||||
{
|
||||
errors++;
|
||||
Debug.LogError($"[EnemyRespawner] {path}:_enemyPrefab 与 _poolKey 均为空,无法生成敌人。", r.gameObject);
|
||||
}
|
||||
|
||||
if (hasKillId && !hasWS)
|
||||
{
|
||||
warnings++;
|
||||
Debug.LogWarning($"[EnemyRespawner] {path}:_persistentKillId=\"{killId.stringValue}\" 但 _worldState 未关联,击杀记录无法持久化。", r.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
string msg = errors == 0 && warnings == 0
|
||||
? $"全部 {all.Length} 个 EnemyRespawner 配置正常。"
|
||||
: $"检查完成:{errors} 个错误,{warnings} 个警告(共 {all.Length} 个)。\n详细信息见 Console 日志。";
|
||||
|
||||
EditorUtility.DisplayDialog("验证结果", msg, "确定");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 25ae5943339e21845a4f350aac520224
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
11
Assets/_Game/Scripts/Editor/Modules/StreamingModule.cs.meta
Normal file
11
Assets/_Game/Scripts/Editor/Modules/StreamingModule.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 43629b8180950e742b8f92d89180c9ac
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -388,8 +388,35 @@ namespace BaseGames.Editor
|
||||
roomSO.ApplyModifiedPropertiesWithoutUndo();
|
||||
}
|
||||
|
||||
// ── MapRoomDataSO 模板(幂等:同名资产已存在时跳过)──────────────
|
||||
string roomIdHint = SceneManager.GetActiveScene().name;
|
||||
if (string.IsNullOrEmpty(roomIdHint) || roomIdHint == "Untitled")
|
||||
roomIdHint = "Room_Unknown";
|
||||
|
||||
string roomSoFolder = "Assets/_Game/Data/Map/Rooms";
|
||||
string roomSoPath = $"{roomSoFolder}/{roomIdHint}.asset";
|
||||
|
||||
bool roomSoCreated = false;
|
||||
if (!System.IO.File.Exists(Application.dataPath + "/../" + roomSoPath))
|
||||
{
|
||||
EnsureFolder(roomSoFolder);
|
||||
var roomData = ScriptableObject.CreateInstance<MapRoomDataSO>();
|
||||
roomData.RoomId = roomIdHint;
|
||||
roomData.RegionId = roomIdHint.Contains("_")
|
||||
? roomIdHint.Split('_')[1]
|
||||
: "";
|
||||
AssetDatabase.CreateAsset(roomData, roomSoPath);
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
roomSoCreated = true;
|
||||
}
|
||||
|
||||
// ── Report ─────────────────────────────────────────────────────
|
||||
report.Add("在 RoomController._roomId 填写唯一房间 ID(如 \"Room_Forest_01\")。");
|
||||
if (roomSoCreated)
|
||||
report.Add($"已自动创建 MapRoomDataSO 模板:{roomSoPath}。请填写 GridPosition / GridSize / Exits,并将其添加到 MapDatabaseSO.AllRooms 中。");
|
||||
else
|
||||
report.Add($"MapRoomDataSO 已存在({roomSoPath}),请确认其 RoomId / Exits 与场景保持同步。");
|
||||
report.Add("绑定 LensConfig SO 后单击 Inspector 中「从可视区域更新限位区域」计算正确的 BoxCollider。");
|
||||
report.Add("使用 Tile Palette 在 Ground Tilemap 上绘制地形,然后在 NavSurface Inspector 中点击 Bake。");
|
||||
report.Add("[Transitions] 子节点下使用 BaseGames/Scene/Place/Room Transition 添加过渡点。");
|
||||
@@ -427,9 +454,10 @@ namespace BaseGames.Editor
|
||||
AssignReference(streamingMgr, "_budget", budgetConfig);
|
||||
AssignAsset(streamingMgr, "_onRoomEntered", report, false, "EVT_RoomEntered");
|
||||
AssignAsset(streamingMgr, "_onRoomPreloaded", report, false, "EVT_RoomPreloaded");
|
||||
AssignAsset(streamingMgr, "_onRoomActivated", report, false, "EVT_RoomActivated");
|
||||
|
||||
// ── TransitionDirector 字段 ───────────────────────────────────────
|
||||
AssignReference(transitionDir, "_streamingManager", streamingMgr);
|
||||
AssignReference(transitionDir, "_streamingManagerRef", streamingMgr);
|
||||
AssignReference(transitionDir, "_mapDatabase", mapDbAsset);
|
||||
AssignReference(transitionDir, "_budget", budgetConfig);
|
||||
AssignAsset(transitionDir, "_onFadeOutRequest", report, false, "EVT_FadeOutRequest");
|
||||
|
||||
175
Assets/_Game/Scripts/Editor/World/RoomTransitionEditor.cs
Normal file
175
Assets/_Game/Scripts/Editor/World/RoomTransitionEditor.cs
Normal file
@@ -0,0 +1,175 @@
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.World;
|
||||
using BaseGames.World.Map;
|
||||
|
||||
namespace BaseGames.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// RoomTransition 自定义 Inspector。
|
||||
/// <para>
|
||||
/// 在默认字段下方附加「MapDatabase 同步」区块:
|
||||
/// 自动查找场景内的 <see cref="RoomController"/>,定位所属房间在 <see cref="MapDatabaseSO"/>
|
||||
/// 中对应出口的 <see cref="RoomExitData.PreferredTransitionType"/>,若与组件
|
||||
/// <c>_transitionType</c> 不一致则显示警告并提供一键同步按钮。
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[CustomEditor(typeof(RoomTransition))]
|
||||
public class RoomTransitionEditor : UnityEditor.Editor
|
||||
{
|
||||
// 每次 Inspector 打开后按需查找一次,避免每帧搜索。
|
||||
private MapDatabaseSO _cachedMapDb;
|
||||
private bool _mapDbSearched;
|
||||
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
DrawDefaultInspector();
|
||||
|
||||
EditorGUILayout.Space(8f);
|
||||
DrawSyncSection();
|
||||
}
|
||||
|
||||
private void DrawSyncSection()
|
||||
{
|
||||
var targetAddrProp = serializedObject.FindProperty("_targetSceneAddress");
|
||||
var typeProp = serializedObject.FindProperty("_transitionType");
|
||||
if (targetAddrProp == null || typeProp == null) return;
|
||||
|
||||
string targetAddr = targetAddrProp.stringValue;
|
||||
if (string.IsNullOrEmpty(targetAddr)) return;
|
||||
|
||||
// 在同一场景内找 RoomController(多场景 Additive 时仅匹配本对象所在场景)
|
||||
string sourceRoomId = FindSourceRoomId(((RoomTransition)target).gameObject);
|
||||
if (string.IsNullOrEmpty(sourceRoomId)) return;
|
||||
|
||||
var mapDb = GetMapDatabase();
|
||||
if (mapDb == null) return;
|
||||
|
||||
var roomData = mapDb.GetRoom(sourceRoomId);
|
||||
if (roomData?.Exits == null) return;
|
||||
|
||||
TransitionType? soType = null;
|
||||
foreach (var exit in roomData.Exits)
|
||||
{
|
||||
if (exit.TargetRoomId == targetAddr)
|
||||
{
|
||||
soType = exit.PreferredTransitionType;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!soType.HasValue) return;
|
||||
|
||||
var currentType = (TransitionType)typeProp.intValue;
|
||||
|
||||
EditorGUILayout.LabelField("MapDatabase 同步", EditorStyles.boldLabel);
|
||||
|
||||
if (currentType == soType.Value)
|
||||
{
|
||||
var prev = GUI.color;
|
||||
GUI.color = new Color(0.4f, 0.9f, 0.4f);
|
||||
EditorGUILayout.LabelField($" ✓ 与 MapDatabase 一致({soType.Value})");
|
||||
GUI.color = prev;
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorGUILayout.HelpBox(
|
||||
$"MapDatabase 中出口 → {targetAddr} 的 PreferredTransitionType = {soType.Value}," +
|
||||
$"但当前组件设置为 {currentType}。两者不一致可能导致关卡编辑时信息有误。",
|
||||
MessageType.Warning);
|
||||
|
||||
if (GUILayout.Button($"从 MapDatabase 同步 → {soType.Value}"))
|
||||
{
|
||||
serializedObject.Update();
|
||||
typeProp.intValue = (int)soType.Value;
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>在与 <paramref name="go"/> 相同场景内找到 RoomController 并返回其 RoomId。</summary>
|
||||
private static string FindSourceRoomId(GameObject go)
|
||||
{
|
||||
var scene = go.scene;
|
||||
if (!scene.IsValid()) return null;
|
||||
|
||||
foreach (var root in scene.GetRootGameObjects())
|
||||
{
|
||||
var ctrl = root.GetComponentInChildren<RoomController>(true);
|
||||
if (ctrl != null) return ctrl.RoomId;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private MapDatabaseSO GetMapDatabase()
|
||||
{
|
||||
if (_mapDbSearched) return _cachedMapDb;
|
||||
_mapDbSearched = true;
|
||||
var guids = AssetDatabase.FindAssets("t:MapDatabaseSO");
|
||||
if (guids.Length > 0)
|
||||
_cachedMapDb = AssetDatabase.LoadAssetAtPath<MapDatabaseSO>(
|
||||
AssetDatabase.GUIDToAssetPath(guids[0]));
|
||||
return _cachedMapDb;
|
||||
}
|
||||
|
||||
// ── 批量验证菜单项 ────────────────────────────────────────────────────────
|
||||
|
||||
[MenuItem("BaseGames/Scene/Validate Transition Types")]
|
||||
private static void ValidateAllTransitionTypes()
|
||||
{
|
||||
var guids = AssetDatabase.FindAssets("t:MapDatabaseSO");
|
||||
if (guids.Length == 0)
|
||||
{
|
||||
EditorUtility.DisplayDialog("验证失败", "未找到 MapDatabaseSO 资产,请先创建。", "确定");
|
||||
return;
|
||||
}
|
||||
|
||||
var mapDb = AssetDatabase.LoadAssetAtPath<MapDatabaseSO>(
|
||||
AssetDatabase.GUIDToAssetPath(guids[0]));
|
||||
|
||||
var transitions = Object.FindObjectsByType<RoomTransition>(
|
||||
FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||||
|
||||
int mismatch = 0;
|
||||
int skipped = 0;
|
||||
|
||||
foreach (var t in transitions)
|
||||
{
|
||||
var so = new SerializedObject(t);
|
||||
var targetProp = so.FindProperty("_targetSceneAddress");
|
||||
var typeProp = so.FindProperty("_transitionType");
|
||||
if (targetProp == null || typeProp == null) continue;
|
||||
|
||||
string sourceRoomId = FindSourceRoomId(t.gameObject);
|
||||
if (string.IsNullOrEmpty(sourceRoomId)) { skipped++; continue; }
|
||||
|
||||
var roomData = mapDb.GetRoom(sourceRoomId);
|
||||
if (roomData?.Exits == null) { skipped++; continue; }
|
||||
|
||||
string targetAddr = targetProp.stringValue;
|
||||
TransitionType? soType = null;
|
||||
foreach (var exit in roomData.Exits)
|
||||
{
|
||||
if (exit.TargetRoomId == targetAddr) { soType = exit.PreferredTransitionType; break; }
|
||||
}
|
||||
if (!soType.HasValue) { skipped++; continue; }
|
||||
|
||||
var currentType = (TransitionType)typeProp.intValue;
|
||||
if (currentType != soType.Value)
|
||||
{
|
||||
mismatch++;
|
||||
Debug.LogWarning(
|
||||
$"[TransitionType 不一致] {t.gameObject.scene.name} / {t.gameObject.name}:" +
|
||||
$"MapDatabase={soType.Value} 组件={currentType}",
|
||||
t.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
string msg = mismatch == 0
|
||||
? $"全部 {transitions.Length} 个 RoomTransition 与 MapDatabase 一致(跳过 {skipped} 个)。"
|
||||
: $"发现 {mismatch} 处类型不一致(跳过 {skipped} 个)。详细信息见 Console 日志。";
|
||||
|
||||
EditorUtility.DisplayDialog("验证结果", msg, "确定");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bb4315fd38131af4a8a42ddfb7b82151
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
625
Assets/_Game/Scripts/Editor/World/StreamingGraphWindow.cs
Normal file
625
Assets/_Game/Scripts/Editor/World/StreamingGraphWindow.cs
Normal file
@@ -0,0 +1,625 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.AddressableAssets;
|
||||
using UnityEngine;
|
||||
using BaseGames.World;
|
||||
using BaseGames.World.Map;
|
||||
using BaseGames.World.Streaming;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
namespace BaseGames.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// 房间连通性可视化编辑器窗口。
|
||||
/// <para>
|
||||
/// 功能:
|
||||
/// <list type="bullet">
|
||||
/// <item>读取 <see cref="MapDatabaseSO"/> 绘制全图房间节点(以 GridPosition/GridSize 定位)</item>
|
||||
/// <item>绘制出口连线,颜色按 <see cref="TransitionType"/> 区分</item>
|
||||
/// <item>节点边框颜色 = Addressable 注册状态(绿色已注册 / 红色未注册)</item>
|
||||
/// <item>运行时叠加:在 Play Mode 下按 <see cref="RoomState"/> 着色节点填充</item>
|
||||
/// <item>单击节点 → Ping SO;双击 → 打开对应场景</item>
|
||||
/// <item>支持鼠标滚轮缩放(向光标缩放)和中键拖拽平移</item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>菜单:<c>BaseGames/World/Streaming Graph</c></para>
|
||||
/// </summary>
|
||||
public class StreamingGraphWindow : EditorWindow
|
||||
{
|
||||
// ── 常量 ───────────────────────────────────────────────────────────────────
|
||||
|
||||
private const float ToolbarH = 28f;
|
||||
private const float StatusBarH = 22f;
|
||||
private const float CellSizePx = 20f; // 每格默认像素数
|
||||
private const float MinNodeW = 64f;
|
||||
private const float MinNodeH = 28f;
|
||||
private const float LegendRowH = 16f; // 图例行高
|
||||
|
||||
// 颜色:过渡类型(连线)
|
||||
private static readonly Color ColorSeamless = new(0.2f, 0.9f, 0.9f);
|
||||
private static readonly Color ColorAtmospheric = new(0.8f, 0.4f, 1.0f);
|
||||
private static readonly Color ColorRoomType = new(0.8f, 0.8f, 0.8f);
|
||||
private static readonly Color ColorSceneType = new(1.0f, 0.6f, 0.1f);
|
||||
|
||||
// 颜色:Addressable 注册状态(节点边框)
|
||||
private static readonly Color ColorRegistered = new(0.2f, 0.9f, 0.2f);
|
||||
private static readonly Color ColorUnregistered = new(0.9f, 0.2f, 0.2f);
|
||||
|
||||
// 颜色:运行时房间状态(节点填充)
|
||||
private static readonly Color ColorActive = new(0.1f, 0.7f, 0.2f, 0.85f);
|
||||
private static readonly Color ColorDormant = new(0.2f, 0.4f, 0.7f, 0.75f);
|
||||
private static readonly Color ColorLoading = new(0.9f, 0.8f, 0.1f, 0.75f);
|
||||
private static readonly Color ColorCooling = new(0.9f, 0.5f, 0.1f, 0.75f);
|
||||
private static readonly Color ColorActivating = new(0.3f, 0.8f, 0.4f, 0.75f);
|
||||
private static readonly Color ColorUnloading = new(0.7f, 0.2f, 0.2f, 0.75f);
|
||||
private static readonly Color ColorDefaultFill = new(0.25f, 0.25f, 0.28f, 0.95f);
|
||||
private static readonly Color ColorCanvasBg = new(0.15f, 0.15f, 0.17f, 1.0f);
|
||||
private static readonly Color ColorBoss = new(1.0f, 0.3f, 0.3f);
|
||||
|
||||
// ── 运行时状态 ────────────────────────────────────────────────────────────
|
||||
|
||||
private MapDatabaseSO _mapDb;
|
||||
private List<NodeData> _nodes = new();
|
||||
private Dictionary<string, NodeData> _nodeIndex = new(); // O(1) roomId→node 索引
|
||||
private IRoomStreamingManager _cachedMgr; // Play Mode 缓存,避免每帧扫描
|
||||
private Vector2 _offset = new(20f, 20f);
|
||||
private float _zoom = 1f;
|
||||
private bool _isPanning;
|
||||
private string _regionFilter = "";
|
||||
private string[] _regions = System.Array.Empty<string>();
|
||||
private int _regionIndex; // 0 = 全部
|
||||
private bool _showLegend = true;
|
||||
private GUIStyle _labelStyle;
|
||||
|
||||
// ── 菜单 / 打开 ───────────────────────────────────────────────────────────
|
||||
|
||||
[MenuItem("BaseGames/World/Streaming Graph")]
|
||||
public static void Open()
|
||||
{
|
||||
var win = GetWindow<StreamingGraphWindow>("Streaming Graph");
|
||||
win.minSize = new Vector2(400f, 300f);
|
||||
win.Refresh();
|
||||
}
|
||||
|
||||
// ── 数据类 ────────────────────────────────────────────────────────────────
|
||||
|
||||
private class NodeData
|
||||
{
|
||||
public MapRoomDataSO So;
|
||||
public string RoomId;
|
||||
public bool IsRegistered;
|
||||
public RoomState? RuntimeState; // null = 编辑模式或未在流式系统中
|
||||
}
|
||||
|
||||
// ── Unity 回调 ────────────────────────────────────────────────────────────
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
// Play Mode 进入/退出时自动刷新 Addressable + 运行时状态
|
||||
EditorApplication.playModeStateChanged += OnPlayModeChanged;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
EditorApplication.playModeStateChanged -= OnPlayModeChanged;
|
||||
}
|
||||
|
||||
private void OnPlayModeChanged(PlayModeStateChange change)
|
||||
{
|
||||
if (change == PlayModeStateChange.EnteredPlayMode ||
|
||||
change == PlayModeStateChange.EnteredEditMode)
|
||||
{
|
||||
_cachedMgr = null;
|
||||
Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
InitStyles();
|
||||
|
||||
DrawToolbar();
|
||||
|
||||
var canvasRect = new Rect(0f, ToolbarH, position.width, position.height - ToolbarH - StatusBarH);
|
||||
|
||||
if (_mapDb == null)
|
||||
{
|
||||
EditorGUI.DrawRect(canvasRect, ColorCanvasBg);
|
||||
GUI.Label(
|
||||
new Rect(canvasRect.center.x - 120f, canvasRect.center.y - 10f, 240f, 20f),
|
||||
"未找到 MapDatabaseSO,请先创建并点击刷新。");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Application.isPlaying)
|
||||
UpdateRuntimeStates();
|
||||
|
||||
DrawCanvas(canvasRect);
|
||||
}
|
||||
|
||||
DrawStatusBar();
|
||||
|
||||
// 持续 Repaint,保持运行时状态颜色实时更新
|
||||
if (Application.isPlaying)
|
||||
Repaint();
|
||||
}
|
||||
|
||||
// ── 工具栏 ────────────────────────────────────────────────────────────────
|
||||
|
||||
private void DrawToolbar()
|
||||
{
|
||||
using var _ = new EditorGUILayout.HorizontalScope(EditorStyles.toolbar,
|
||||
GUILayout.Height(ToolbarH));
|
||||
|
||||
if (GUILayout.Button("刷新", EditorStyles.toolbarButton, GUILayout.Width(48)))
|
||||
Refresh();
|
||||
|
||||
if (GUILayout.Button("居中", EditorStyles.toolbarButton, GUILayout.Width(48)))
|
||||
CenterView();
|
||||
|
||||
GUILayout.Label("区域:", EditorStyles.miniLabel, GUILayout.Width(36));
|
||||
int newIdx = EditorGUILayout.Popup(_regionIndex, _regions,
|
||||
EditorStyles.toolbarPopup, GUILayout.Width(100));
|
||||
if (newIdx != _regionIndex)
|
||||
{
|
||||
_regionIndex = newIdx;
|
||||
_regionFilter = newIdx == 0 ? "" : _regions[newIdx];
|
||||
BuildNodeCache();
|
||||
}
|
||||
|
||||
GUILayout.FlexibleSpace();
|
||||
|
||||
// 缩放显示
|
||||
GUILayout.Label($"缩放:{_zoom:F2}×", EditorStyles.miniLabel, GUILayout.Width(72));
|
||||
|
||||
_showLegend = GUILayout.Toggle(_showLegend, "图例", EditorStyles.toolbarButton,
|
||||
GUILayout.Width(42));
|
||||
}
|
||||
|
||||
// ── 画布 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
private void DrawCanvas(Rect canvasRect)
|
||||
{
|
||||
// 背景
|
||||
if (Event.current.type == EventType.Repaint)
|
||||
EditorGUI.DrawRect(canvasRect, ColorCanvasBg);
|
||||
|
||||
HandleCanvasInput(canvasRect);
|
||||
|
||||
if (Event.current.type != EventType.Repaint)
|
||||
return;
|
||||
|
||||
// 先绘制连线(在节点层下面)
|
||||
Handles.BeginGUI();
|
||||
DrawEdges(canvasRect);
|
||||
Handles.EndGUI();
|
||||
|
||||
// 绘制节点填充 + 标签(GUI.BeginClip 裁剪至画布范围)
|
||||
GUI.BeginClip(canvasRect);
|
||||
DrawNodeFills(canvasRect);
|
||||
GUI.EndClip();
|
||||
|
||||
// 绘制节点边框(Handles,使用绝对坐标)
|
||||
Handles.BeginGUI();
|
||||
DrawNodeBorders(canvasRect);
|
||||
Handles.EndGUI();
|
||||
|
||||
if (_showLegend)
|
||||
DrawLegend(canvasRect);
|
||||
}
|
||||
|
||||
private void HandleCanvasInput(Rect canvasRect)
|
||||
{
|
||||
var e = Event.current;
|
||||
if (!canvasRect.Contains(e.mousePosition))
|
||||
{
|
||||
if (_isPanning && e.type == EventType.MouseUp)
|
||||
_isPanning = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 滚轮缩放(向光标点缩放)
|
||||
if (e.type == EventType.ScrollWheel)
|
||||
{
|
||||
float prevZoom = _zoom;
|
||||
_zoom = Mathf.Clamp(_zoom * (1f - e.delta.y * 0.05f), 0.15f, 5f);
|
||||
var cursor = e.mousePosition - canvasRect.min;
|
||||
_offset = cursor - (_zoom / prevZoom) * (cursor - _offset);
|
||||
e.Use();
|
||||
Repaint();
|
||||
return;
|
||||
}
|
||||
|
||||
// 中键按下 / 松开
|
||||
if (e.type == EventType.MouseDown && e.button == 2) { _isPanning = true; e.Use(); }
|
||||
if (e.type == EventType.MouseUp && e.button == 2) { _isPanning = false; e.Use(); }
|
||||
|
||||
// 中键拖拽平移
|
||||
if (e.type == EventType.MouseDrag && _isPanning)
|
||||
{
|
||||
_offset += e.delta;
|
||||
e.Use();
|
||||
Repaint();
|
||||
return;
|
||||
}
|
||||
|
||||
// 左键点击节点
|
||||
if (e.type == EventType.MouseDown && e.button == 0)
|
||||
{
|
||||
foreach (var node in _nodes)
|
||||
{
|
||||
var r = GetAbsNodeRect(node, canvasRect);
|
||||
if (!r.Contains(e.mousePosition)) continue;
|
||||
|
||||
if (e.clickCount == 2)
|
||||
OpenRoomScene(node.RoomId);
|
||||
else
|
||||
{
|
||||
Selection.activeObject = node.So;
|
||||
EditorGUIUtility.PingObject(node.So);
|
||||
}
|
||||
e.Use();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 绘制:连线 ────────────────────────────────────────────────────────────
|
||||
|
||||
private void DrawEdges(Rect canvasRect)
|
||||
{
|
||||
foreach (var node in _nodes)
|
||||
{
|
||||
if (node.So.Exits == null) continue;
|
||||
foreach (var exit in node.So.Exits)
|
||||
{
|
||||
if (!_nodeIndex.TryGetValue(exit.TargetRoomId, out var targetNode)) continue;
|
||||
|
||||
// 从出口格子坐标到目标房间中心
|
||||
var from = canvasRect.min + CanvasLocalPos(exit.ExitGridPos.x, exit.ExitGridPos.y);
|
||||
var to = GetAbsNodeRect(targetNode, canvasRect).center;
|
||||
|
||||
Handles.color = GetTransitionColor(exit.PreferredTransitionType);
|
||||
Handles.DrawLine(from, to);
|
||||
DrawArrowHead(from, to, 6f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawArrowHead(Vector2 from, Vector2 to, float size)
|
||||
{
|
||||
var dir = (to - from).normalized;
|
||||
if (dir == Vector2.zero) return;
|
||||
var perp = new Vector2(-dir.y, dir.x);
|
||||
var tip = to;
|
||||
var base1 = tip - dir * size + perp * (size * 0.5f);
|
||||
var base2 = tip - dir * size - perp * (size * 0.5f);
|
||||
Handles.DrawLine(tip, base1);
|
||||
Handles.DrawLine(tip, base2);
|
||||
}
|
||||
|
||||
// ── 绘制:节点填充(BeginClip 内,画布局部坐标) ─────────────────────────
|
||||
|
||||
private void DrawNodeFills(Rect canvasRect)
|
||||
{
|
||||
foreach (var node in _nodes)
|
||||
{
|
||||
// 在 BeginClip 内:坐标相对于 canvasRect.min
|
||||
var localRect = GetLocalNodeRect(node);
|
||||
if (!new Rect(0, 0, canvasRect.width, canvasRect.height).Overlaps(localRect))
|
||||
continue;
|
||||
|
||||
EditorGUI.DrawRect(localRect, GetStateFillColor(node.RuntimeState));
|
||||
|
||||
// Boss 房间底部红色条
|
||||
if (node.So.IsBossRoom)
|
||||
{
|
||||
var markerRect = new Rect(localRect.x, localRect.yMax - 3f, localRect.width, 3f);
|
||||
EditorGUI.DrawRect(markerRect, ColorBoss);
|
||||
}
|
||||
|
||||
// 标签
|
||||
var label = BuildLabel(node);
|
||||
GUI.Label(localRect, label, _labelStyle);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 绘制:节点边框(Handles,绝对坐标) ──────────────────────────────────
|
||||
|
||||
private void DrawNodeBorders(Rect canvasRect)
|
||||
{
|
||||
foreach (var node in _nodes)
|
||||
{
|
||||
var r = GetAbsNodeRect(node, canvasRect);
|
||||
if (!canvasRect.Overlaps(r)) continue;
|
||||
|
||||
var color = node.IsRegistered ? ColorRegistered : ColorUnregistered;
|
||||
DrawHandleRect(r, color);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawHandleRect(Rect r, Color c)
|
||||
{
|
||||
Handles.color = c;
|
||||
var tl = new Vector3(r.xMin, r.yMin);
|
||||
var tr = new Vector3(r.xMax, r.yMin);
|
||||
var br = new Vector3(r.xMax, r.yMax);
|
||||
var bl = new Vector3(r.xMin, r.yMax);
|
||||
Handles.DrawLine(tl, tr);
|
||||
Handles.DrawLine(tr, br);
|
||||
Handles.DrawLine(br, bl);
|
||||
Handles.DrawLine(bl, tl);
|
||||
}
|
||||
|
||||
// ── 图例 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
private void DrawLegend(Rect canvasRect)
|
||||
{
|
||||
const float W = 170f;
|
||||
const float H = 148f;
|
||||
const float Px = 8f;
|
||||
const float Py = 8f;
|
||||
|
||||
var legendRect = new Rect(canvasRect.xMax - W - Px, canvasRect.y + Py, W, H);
|
||||
EditorGUI.DrawRect(legendRect, new Color(0.1f, 0.1f, 0.12f, 0.88f));
|
||||
|
||||
float y = legendRect.y + 6f;
|
||||
LegendRow(ref y, legendRect.x + 6f, "连线颜色(过渡类型)", Color.white, isHeader: true);
|
||||
LegendRow(ref y, legendRect.x + 6f, " Seamless", ColorSeamless);
|
||||
LegendRow(ref y, legendRect.x + 6f, " AtmosphericFade", ColorAtmospheric);
|
||||
LegendRow(ref y, legendRect.x + 6f, " Room", ColorRoomType);
|
||||
LegendRow(ref y, legendRect.x + 6f, " Scene", ColorSceneType);
|
||||
LegendRow(ref y, legendRect.x + 6f, "边框(Addressable)", Color.white, isHeader: true);
|
||||
LegendRow(ref y, legendRect.x + 6f, " 已注册", ColorRegistered);
|
||||
LegendRow(ref y, legendRect.x + 6f, " 未注册", ColorUnregistered);
|
||||
|
||||
Handles.BeginGUI();
|
||||
DrawHandleRect(legendRect, new Color(0.4f, 0.4f, 0.45f));
|
||||
Handles.EndGUI();
|
||||
}
|
||||
|
||||
private static void LegendRow(ref float y, float x, string label, Color swatch,
|
||||
bool isHeader = false)
|
||||
{
|
||||
if (!isHeader)
|
||||
{
|
||||
EditorGUI.DrawRect(new Rect(x, y + 3f, 12f, 10f), swatch);
|
||||
x += 16f;
|
||||
}
|
||||
GUI.Label(new Rect(x, y, 160f, LegendRowH), label,
|
||||
isHeader ? EditorStyles.boldLabel : EditorStyles.label);
|
||||
y += LegendRowH;
|
||||
}
|
||||
|
||||
// ── 状态栏 ────────────────────────────────────────────────────────────────
|
||||
|
||||
private void DrawStatusBar()
|
||||
{
|
||||
var statusRect = new Rect(0f, position.height - StatusBarH, position.width, StatusBarH);
|
||||
EditorGUI.DrawRect(statusRect, new Color(0.12f, 0.12f, 0.14f));
|
||||
|
||||
int totalEdges = _nodes.Sum(n => n.So?.Exits?.Length ?? 0);
|
||||
int unregistered = _nodes.Count(n => !n.IsRegistered);
|
||||
string playing = Application.isPlaying ? " ● Play Mode" : "";
|
||||
|
||||
var rooms = _mapDb?.AllRooms;
|
||||
int filteredCount = _nodes.Count;
|
||||
int totalCount = rooms?.Length ?? 0;
|
||||
string roomsStr = _regionFilter.Length > 0
|
||||
? $"{filteredCount} / {totalCount} 房间"
|
||||
: $"{totalCount} 房间";
|
||||
|
||||
string text = $" {roomsStr} · {totalEdges} 出口 · {unregistered} 未注册{playing}";
|
||||
GUI.Label(new Rect(0f, position.height - StatusBarH, position.width, StatusBarH), text);
|
||||
}
|
||||
|
||||
// ── 坐标工具 ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>网格坐标 → 画布局部偏移(不含 canvasRect.min)。</summary>
|
||||
private Vector2 CanvasLocalPos(float gridX, float gridY)
|
||||
=> _offset + new Vector2(gridX * CellSizePx * _zoom, gridY * CellSizePx * _zoom);
|
||||
|
||||
/// <summary>房间节点的画布局部 Rect(相对于 canvasRect.min,供 BeginClip 内使用)。</summary>
|
||||
private Rect GetLocalNodeRect(NodeData node)
|
||||
{
|
||||
var pos = CanvasLocalPos(node.So.GridPosition.x, node.So.GridPosition.y);
|
||||
float w = Mathf.Max(node.So.GridSize.x * CellSizePx * _zoom, MinNodeW);
|
||||
float h = Mathf.Max(node.So.GridSize.y * CellSizePx * _zoom, MinNodeH);
|
||||
return new Rect(pos.x, pos.y, w, h);
|
||||
}
|
||||
|
||||
/// <summary>房间节点的绝对 Rect(含 canvasRect.min,供 Handles 使用)。</summary>
|
||||
private Rect GetAbsNodeRect(NodeData node, Rect canvasRect)
|
||||
{
|
||||
var local = GetLocalNodeRect(node);
|
||||
return new Rect(canvasRect.min + local.min, local.size);
|
||||
}
|
||||
|
||||
// ── 数据刷新 ──────────────────────────────────────────────────────────────
|
||||
|
||||
private void Refresh()
|
||||
{
|
||||
_mapDb = null;
|
||||
var guids = AssetDatabase.FindAssets("t:MapDatabaseSO");
|
||||
if (guids.Length > 0)
|
||||
_mapDb = AssetDatabase.LoadAssetAtPath<MapDatabaseSO>(
|
||||
AssetDatabase.GUIDToAssetPath(guids[0]));
|
||||
|
||||
BuildRegions();
|
||||
BuildNodeCache();
|
||||
CheckAddressableRegistration();
|
||||
Repaint();
|
||||
}
|
||||
|
||||
private void BuildRegions()
|
||||
{
|
||||
var set = new HashSet<string> { "" };
|
||||
if (_mapDb?.AllRooms != null)
|
||||
foreach (var r in _mapDb.AllRooms)
|
||||
if (r != null && !string.IsNullOrEmpty(r.RegionId))
|
||||
set.Add(r.RegionId);
|
||||
|
||||
var list = new List<string>(set);
|
||||
list.Sort();
|
||||
int allIdx = list.IndexOf("");
|
||||
if (allIdx >= 0) list.RemoveAt(allIdx);
|
||||
list.Insert(0, "全部");
|
||||
|
||||
_regions = list.ToArray();
|
||||
// 第 0 项 = "全部"(对应空字符串 _regionFilter),后续是各 RegionId
|
||||
if (_regionIndex >= _regions.Length) _regionIndex = 0;
|
||||
}
|
||||
|
||||
private void BuildNodeCache()
|
||||
{
|
||||
_nodes.Clear();
|
||||
_nodeIndex.Clear();
|
||||
if (_mapDb?.AllRooms == null) return;
|
||||
|
||||
foreach (var room in _mapDb.AllRooms)
|
||||
{
|
||||
if (room == null) continue;
|
||||
if (!string.IsNullOrEmpty(_regionFilter) && room.RegionId != _regionFilter)
|
||||
continue;
|
||||
var node = new NodeData { So = room, RoomId = room.RoomId };
|
||||
_nodes.Add(node);
|
||||
_nodeIndex[room.RoomId] = node;
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckAddressableRegistration()
|
||||
{
|
||||
var settings = AddressableAssetSettingsDefaultObject.Settings;
|
||||
var registered = new HashSet<string>();
|
||||
|
||||
if (settings != null)
|
||||
foreach (var group in settings.groups)
|
||||
{
|
||||
if (group == null) continue;
|
||||
foreach (var entry in group.entries)
|
||||
registered.Add(entry.address);
|
||||
}
|
||||
|
||||
foreach (var node in _nodes)
|
||||
node.IsRegistered = registered.Contains(node.RoomId);
|
||||
}
|
||||
|
||||
private void UpdateRuntimeStates()
|
||||
{
|
||||
// 缓存 IRoomStreamingManager 引用,避免每帧扫描全场景对象
|
||||
if (_cachedMgr == null)
|
||||
_cachedMgr = Object.FindAnyObjectByType<RoomStreamingManager>() as IRoomStreamingManager;
|
||||
|
||||
foreach (var node in _nodes)
|
||||
node.RuntimeState = _cachedMgr?.GetRoomState(node.RoomId);
|
||||
}
|
||||
|
||||
// ── 视图工具 ──────────────────────────────────────────────────────────────
|
||||
|
||||
private void CenterView()
|
||||
{
|
||||
if (_nodes.Count == 0) return;
|
||||
|
||||
int minX = int.MaxValue, minY = int.MaxValue;
|
||||
int maxX = int.MinValue, maxY = int.MinValue;
|
||||
|
||||
foreach (var n in _nodes)
|
||||
{
|
||||
minX = Mathf.Min(minX, n.So.GridPosition.x);
|
||||
minY = Mathf.Min(minY, n.So.GridPosition.y);
|
||||
maxX = Mathf.Max(maxX, n.So.GridPosition.x + Mathf.Max(n.So.GridSize.x, 1));
|
||||
maxY = Mathf.Max(maxY, n.So.GridPosition.y + Mathf.Max(n.So.GridSize.y, 1));
|
||||
}
|
||||
|
||||
int totalGridW = maxX - minX;
|
||||
int totalGridH = maxY - minY;
|
||||
|
||||
float canvasW = position.width;
|
||||
float canvasH = position.height - ToolbarH - StatusBarH;
|
||||
|
||||
float zoomX = (canvasW * 0.8f) / Mathf.Max(totalGridW * CellSizePx, 1f);
|
||||
float zoomY = (canvasH * 0.8f) / Mathf.Max(totalGridH * CellSizePx, 1f);
|
||||
_zoom = Mathf.Clamp(Mathf.Min(zoomX, zoomY), 0.15f, 3f);
|
||||
|
||||
float scaledW = totalGridW * CellSizePx * _zoom;
|
||||
float scaledH = totalGridH * CellSizePx * _zoom;
|
||||
_offset = new Vector2(
|
||||
(canvasW - scaledW) * 0.5f - minX * CellSizePx * _zoom,
|
||||
(canvasH - scaledH) * 0.5f - minY * CellSizePx * _zoom);
|
||||
|
||||
Repaint();
|
||||
}
|
||||
|
||||
// ── 辅助 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
private void InitStyles()
|
||||
{
|
||||
if (_labelStyle != null) return;
|
||||
_labelStyle = new GUIStyle(EditorStyles.label)
|
||||
{
|
||||
alignment = TextAnchor.MiddleCenter,
|
||||
fontSize = 9,
|
||||
wordWrap = true,
|
||||
normal = { textColor = new Color(0.92f, 0.92f, 0.92f) }
|
||||
};
|
||||
}
|
||||
|
||||
private static Color GetStateFillColor(RoomState? state)
|
||||
{
|
||||
if (!state.HasValue) return ColorDefaultFill;
|
||||
return state.Value switch
|
||||
{
|
||||
RoomState.Active => ColorActive,
|
||||
RoomState.Activating => ColorActivating,
|
||||
RoomState.Dormant => ColorDormant,
|
||||
RoomState.Loading => ColorLoading,
|
||||
RoomState.Cooling => ColorCooling,
|
||||
RoomState.Unloading => ColorUnloading,
|
||||
_ => ColorDefaultFill
|
||||
};
|
||||
}
|
||||
|
||||
private static Color GetTransitionColor(TransitionType t) => t switch
|
||||
{
|
||||
TransitionType.Seamless => ColorSeamless,
|
||||
TransitionType.AtmosphericFade => ColorAtmospheric,
|
||||
TransitionType.Scene => ColorSceneType,
|
||||
_ => ColorRoomType
|
||||
};
|
||||
|
||||
private static string BuildLabel(NodeData node)
|
||||
{
|
||||
var id = node.RoomId ?? "";
|
||||
// 缩短显示:去掉 "Room_" 前缀
|
||||
if (id.StartsWith("Room_")) id = id[5..];
|
||||
|
||||
string stateTag = node.RuntimeState switch
|
||||
{
|
||||
RoomState.Active => "\n● Active",
|
||||
RoomState.Activating => "\n◑ Activating",
|
||||
RoomState.Dormant => "\n○ Dormant",
|
||||
RoomState.Loading => "\n⟳ Loading",
|
||||
RoomState.Cooling => "\n◌ Cooling",
|
||||
RoomState.Unloading => "\n✕ Unloading",
|
||||
_ => ""
|
||||
};
|
||||
string bossTag = node.So.IsBossRoom ? " [B]" : "";
|
||||
return id + bossTag + stateTag;
|
||||
}
|
||||
|
||||
private static void OpenRoomScene(string roomId)
|
||||
{
|
||||
var guids = AssetDatabase.FindAssets($"t:SceneAsset {roomId}");
|
||||
if (guids.Length == 0)
|
||||
{
|
||||
Debug.LogWarning($"[StreamingGraph] 未找到场景 {roomId}。");
|
||||
return;
|
||||
}
|
||||
var path = AssetDatabase.GUIDToAssetPath(guids[0]);
|
||||
if (UnityEditor.SceneManagement.EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo())
|
||||
UnityEditor.SceneManagement.EditorSceneManager.OpenScene(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ff23fee892b09e6479e2470fbdcbb1b3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user