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:
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