Add independent review reports for Minimap system (Rounds 8, 9, and 26)
- Round 8 report highlights improvements in architecture, editor usability, and data robustness, with a total score of 80/100. - Round 9 report focuses on editor extension capabilities, identifying issues with room data indexing and layout editing, resulting in a score of 76/100. - Round 26 report evaluates the system against commercial standards, noting new issues and confirming previous fixes, with a score of 95.8/100.
This commit is contained in:
@@ -39,6 +39,13 @@ namespace BaseGames.Editor.Map
|
||||
{
|
||||
_database = (MapDatabaseSO)target;
|
||||
_errorRowStyle = null; // 编辑器皮肤切换时(亮/暗模式)需重建
|
||||
// 自动验证:Inspector 打开时即填充错误集,无需手动点击"重新验证"按钮
|
||||
// 防御性 null 检查:CustomEditor.OnEnable 在反序列化失败的资产上仍会调用
|
||||
if (_database != null)
|
||||
{
|
||||
_lastErrors = _database.ValidateAll();
|
||||
RebuildErrorRoomCache();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>错误行 GUIStyle 惰性初始化,基于当前编辑器皮肤构建,避免 OnInspectorGUI 每帧分配。</summary>
|
||||
@@ -50,6 +57,21 @@ namespace BaseGames.Editor.Map
|
||||
return _errorRowStyle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 基于 _lastErrors 重建 _cachedErrorRoomIds 集合。
|
||||
/// 使用引号匹配防止 "Room_A1" 误匹配 "Room_A10"。
|
||||
/// 由 OnEnable 自动验证与"重新验证"按钮共享调用。
|
||||
/// </summary>
|
||||
private void RebuildErrorRoomCache()
|
||||
{
|
||||
_cachedErrorRoomIds.Clear();
|
||||
if (_lastErrors == null || _lastErrors.Count == 0 || _database.AllRooms == null) return;
|
||||
foreach (var err in _lastErrors)
|
||||
foreach (var r in _database.AllRooms)
|
||||
if (r != null && err.Contains($"'{r.RoomId}'"))
|
||||
_cachedErrorRoomIds.Add(r.RoomId);
|
||||
}
|
||||
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
serializedObject.Update();
|
||||
@@ -77,14 +99,7 @@ namespace BaseGames.Editor.Map
|
||||
if (GUILayout.Button(LabelValidate, GUILayout.Height(28)))
|
||||
{
|
||||
_lastErrors = _database.ValidateAll();
|
||||
|
||||
// 构建错误 RoomId 集合(使用引号匹配,防止 "Room_A1" 误匹配 "Room_A10")
|
||||
_cachedErrorRoomIds.Clear();
|
||||
if (_lastErrors.Count > 0 && _database.AllRooms != null)
|
||||
foreach (var err in _lastErrors)
|
||||
foreach (var r in _database.AllRooms)
|
||||
if (r != null && err.Contains($"'{r.RoomId}'"))
|
||||
_cachedErrorRoomIds.Add(r.RoomId);
|
||||
RebuildErrorRoomCache();
|
||||
|
||||
if (_lastErrors.Count == 0)
|
||||
Debug.Log("[MapDatabase] 验证通过,未发现问题 ✓");
|
||||
|
||||
@@ -32,8 +32,20 @@ namespace BaseGames.Editor.Map
|
||||
private List<string> _validationErrors;
|
||||
private HashSet<string> _errorRoomIds;
|
||||
|
||||
/// <summary>缓存的错误房间集合,由验证按钮点击时重建(防止 OnInspectorGUI 高频重建导致 GC)。</summary>
|
||||
private readonly HashSet<string> _cachedErrorRoomIds = new();
|
||||
// R9-N2 房间拖拽编辑状态
|
||||
private MapRoomDataSO _draggedRoom;
|
||||
private Vector2Int _dragOriginGridPos;
|
||||
private Vector2 _dragMouseStart;
|
||||
private bool _dragHasConflict; // R10-N5 当前拖拽位置与其它房间重叠时为 true
|
||||
|
||||
// R9-N4 搜索/图例
|
||||
private string _searchText = string.Empty;
|
||||
private bool _showLegend = true;
|
||||
|
||||
/// <summary>N4: DrawExitLines 去重集,缓存为字段避免每次 OnGUI 分配。</summary>
|
||||
private readonly HashSet<(string, string)> _drawnExitPairs = new();
|
||||
/// <summary>N4: DrawExitLines 连接线颜色,缓存为字段避免每次 OnGUI 分配。</summary>
|
||||
private static readonly Color ExitLineColor = new Color(1f, 1f, 0.5f, 0.35f);
|
||||
|
||||
private readonly Dictionary<string, Color> _regionColors = new();
|
||||
private int _paletteIndex;
|
||||
@@ -42,6 +54,7 @@ namespace BaseGames.Editor.Map
|
||||
private GUIStyle _roomLabelStyle;
|
||||
private GUIStyle _badgeBossStyle;
|
||||
private GUIStyle _badgeNormalStyle;
|
||||
private GUIStyle _noResultStyle; // R18-N2 缓存"搜索无结果"提示样式(zoom 无关,初始化一次)
|
||||
private float _cachedZoomForStyle = -1f; // 用于检测缩放变化时重建 Style
|
||||
|
||||
// 区域配色方案(与 MapExploration 标注颜色视觉呼应)
|
||||
@@ -78,6 +91,14 @@ namespace BaseGames.Editor.Map
|
||||
Repaint();
|
||||
}
|
||||
|
||||
/// <summary>R11-N7 项目资产变更(导入/删除/重命名)后清除验证缓存并触发重绘。</summary>
|
||||
private void OnProjectChange()
|
||||
{
|
||||
_validationErrors = null;
|
||||
_errorRoomIds = null;
|
||||
Repaint();
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
DrawToolbar();
|
||||
@@ -97,6 +118,14 @@ namespace BaseGames.Editor.Map
|
||||
EditorGUILayout.HelpBox(msg, MessageType.Warning);
|
||||
}
|
||||
|
||||
// R10-N5 拖拽冲突提示
|
||||
if (_draggedRoom != null && _dragHasConflict)
|
||||
{
|
||||
EditorGUILayout.HelpBox(
|
||||
$"⚠ 房间 '{_draggedRoom.RoomId}' 当前位置与其它房间格子重叠,请调整。",
|
||||
MessageType.Error);
|
||||
}
|
||||
|
||||
// 地图绘制区域
|
||||
Rect mapRect = GUILayoutUtility.GetRect(
|
||||
GUIContent.none, GUIStyle.none,
|
||||
@@ -137,6 +166,25 @@ namespace BaseGames.Editor.Map
|
||||
Repaint();
|
||||
}
|
||||
|
||||
// R9-N4 / R24-N3 搜索框:输入 RoomId / RegionId 子串或 RoomType 枚举名高亮匹配房间;✕ 按钮一键清空
|
||||
GUILayout.Label("搜索", GUILayout.Width(32));
|
||||
var newSearch = EditorGUILayout.TextField(_searchText, EditorStyles.toolbarTextField, GUILayout.Width(120));
|
||||
if (newSearch != _searchText)
|
||||
{
|
||||
_searchText = newSearch;
|
||||
Repaint();
|
||||
}
|
||||
GUI.enabled = !string.IsNullOrEmpty(_searchText);
|
||||
if (GUILayout.Button("✕", EditorStyles.toolbarButton, GUILayout.Width(22)))
|
||||
{
|
||||
_searchText = string.Empty;
|
||||
GUI.FocusControl(null);
|
||||
Repaint();
|
||||
}
|
||||
GUI.enabled = true;
|
||||
|
||||
_showLegend = GUILayout.Toggle(_showLegend, "图例", EditorStyles.toolbarButton, GUILayout.Width(48));
|
||||
|
||||
EditorGUILayout.LabelField($"缩放 {_zoom:F0}px/格", GUILayout.Width(80));
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
@@ -163,6 +211,23 @@ namespace BaseGames.Editor.Map
|
||||
e.Use();
|
||||
break;
|
||||
|
||||
// R9-N2 左键按下(不带 Alt):若命中房间则进入"房间拖拽"模式
|
||||
case EventType.MouseDown when e.button == 0 && !e.alt:
|
||||
{
|
||||
var hit = HitTestRoom(e.mousePosition - mapRect.position, mapRect);
|
||||
if (hit != null)
|
||||
{
|
||||
_draggedRoom = hit;
|
||||
_dragOriginGridPos = hit.GridPosition;
|
||||
_dragMouseStart = e.mousePosition;
|
||||
_selectedRoom = hit;
|
||||
Selection.activeObject = hit;
|
||||
EditorGUIUtility.PingObject(hit);
|
||||
e.Use();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case EventType.MouseDrag when _isDragging:
|
||||
_panOffset += e.mousePosition - _dragStart;
|
||||
_dragStart = e.mousePosition;
|
||||
@@ -170,18 +235,75 @@ namespace BaseGames.Editor.Map
|
||||
Repaint();
|
||||
break;
|
||||
|
||||
// R9-N2 房间拖拽:将像素偏移换算为格子偏移,实时更新 GridPosition
|
||||
case EventType.MouseDrag when _draggedRoom != null:
|
||||
{
|
||||
Vector2 delta = e.mousePosition - _dragMouseStart;
|
||||
int gx = Mathf.RoundToInt(delta.x / _zoom);
|
||||
int gy = Mathf.RoundToInt(-delta.y / _zoom); // 屏幕 Y 向下→格子 Y 向上
|
||||
var newPos = new Vector2Int(_dragOriginGridPos.x + gx, _dragOriginGridPos.y + gy);
|
||||
if (newPos != _draggedRoom.GridPosition)
|
||||
{
|
||||
Undo.RecordObject(_draggedRoom, "Move Room");
|
||||
_draggedRoom.GridPosition = newPos;
|
||||
EditorUtility.SetDirty(_draggedRoom);
|
||||
// R10-N5 重叠检测:候选位置覆盖的任意格子被其它房间占用即标冲突
|
||||
_dragHasConflict = HasOverlapAt(_draggedRoom, newPos);
|
||||
Repaint();
|
||||
}
|
||||
e.Use();
|
||||
break;
|
||||
}
|
||||
|
||||
case EventType.MouseUp when _isDragging:
|
||||
_isDragging = false;
|
||||
e.Use();
|
||||
break;
|
||||
|
||||
// 左键(非 Alt)点击选中房间
|
||||
case EventType.MouseUp when e.button == 0 && !e.alt:
|
||||
TrySelectRoom(e.mousePosition - mapRect.position, mapRect);
|
||||
// R9-N2 房间拖拽结束
|
||||
case EventType.MouseUp when _draggedRoom != null && e.button == 0:
|
||||
_draggedRoom = null;
|
||||
_dragHasConflict = false;
|
||||
e.Use();
|
||||
Repaint();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>命中测试:返回鼠标位置覆盖的房间(用于选中/拖拽起始判定)。</summary>
|
||||
private MapRoomDataSO HitTestRoom(Vector2 clipPos, Rect mapRect)
|
||||
{
|
||||
if (_database?.AllRooms == null) return null;
|
||||
Vector2 origin = mapRect.size * 0.5f + _panOffset;
|
||||
foreach (var room in _database.AllRooms)
|
||||
{
|
||||
if (room == null) continue;
|
||||
if (RoomToClipRect(room, origin).Contains(clipPos))
|
||||
return room;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R10-N5 检测候选房间放置在 <paramref name="newPos"/> 时是否与其它房间格子重叠。
|
||||
/// 排除自身;用于拖拽时实时给策划红色反馈。
|
||||
/// </summary>
|
||||
private bool HasOverlapAt(MapRoomDataSO room, Vector2Int newPos)
|
||||
{
|
||||
if (_database?.AllRooms == null || room == null) return false;
|
||||
int minX = newPos.x, maxX = newPos.x + room.GridSize.x;
|
||||
int minY = newPos.y, maxY = newPos.y + room.GridSize.y;
|
||||
foreach (var other in _database.AllRooms)
|
||||
{
|
||||
if (other == null || other == room) continue;
|
||||
int oMinX = other.GridPosition.x, oMaxX = oMinX + other.GridSize.x;
|
||||
int oMinY = other.GridPosition.y, oMaxY = oMinY + other.GridSize.y;
|
||||
bool overlap = minX < oMaxX && maxX > oMinX && minY < oMaxY && maxY > oMinY;
|
||||
if (overlap) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── 地图绘制 ──────────────────────────────────────────────────────────
|
||||
|
||||
private void DrawMapArea(Rect mapRect)
|
||||
@@ -193,6 +315,9 @@ namespace BaseGames.Editor.Map
|
||||
|
||||
Vector2 origin = mapRect.size * 0.5f + _panOffset;
|
||||
|
||||
// N3 搜索有内容时追踪是否有任何匹配项,无结果时显示提示
|
||||
bool anySearchMatch = false;
|
||||
|
||||
foreach (var room in _database.AllRooms)
|
||||
{
|
||||
if (room == null) continue;
|
||||
@@ -201,10 +326,25 @@ namespace BaseGames.Editor.Map
|
||||
|
||||
// 填充
|
||||
bool hasError = _errorRoomIds != null && _errorRoomIds.Contains(room.RoomId);
|
||||
// R23-N2 搜索支持 RoomId / RegionId 子串 以及 RoomType 枚举名(如 "SavePoint"、"BossRoom")
|
||||
bool matchesSearch = !string.IsNullOrEmpty(_searchText)
|
||||
&& (room.RoomId?.IndexOf(_searchText, System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| room.RegionId?.IndexOf(_searchText, System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| MatchesRoomType(room.RoomFlags, _searchText));
|
||||
if (matchesSearch) anySearchMatch = true;
|
||||
// R10-N5 正在被拖拽且与其它房间重叠时优先显示红色警示
|
||||
bool draggingConflict = room == _draggedRoom && _dragHasConflict;
|
||||
Color regionColor = GetRegionColor(room.RegionId);
|
||||
Color fillColor = hasError
|
||||
? new Color(1f, 0.15f, 0.15f, 0.55f)
|
||||
: new Color(regionColor.r, regionColor.g, regionColor.b, 0.28f);
|
||||
Color fillColor = draggingConflict
|
||||
? new Color(1f, 0.1f, 0.1f, 0.75f)
|
||||
: hasError
|
||||
? new Color(1f, 0.15f, 0.15f, 0.55f)
|
||||
: matchesSearch
|
||||
? new Color(1f, 0.95f, 0.2f, 0.55f)
|
||||
// N2 搜索活跃时非匹配房间降低 alpha,使匹配项更突出
|
||||
: !string.IsNullOrEmpty(_searchText)
|
||||
? new Color(regionColor.r, regionColor.g, regionColor.b, 0.08f)
|
||||
: new Color(regionColor.r, regionColor.g, regionColor.b, 0.28f);
|
||||
EditorGUI.DrawRect(cell, fillColor);
|
||||
|
||||
// 边框
|
||||
@@ -226,7 +366,75 @@ namespace BaseGames.Editor.Map
|
||||
if (_zoom >= 12f)
|
||||
DrawExitLines(origin);
|
||||
|
||||
// R9-N11 Play Mode 玩家位置叠加:在当前房间上绘制醒目红点
|
||||
if (Application.isPlaying)
|
||||
DrawPlayModePlayerDot(origin);
|
||||
|
||||
// N3 搜索无结果时居中显示提示文本
|
||||
if (!string.IsNullOrEmpty(_searchText) && !anySearchMatch)
|
||||
{
|
||||
GUI.Label(new Rect(0f, 0f, mapRect.width, mapRect.height),
|
||||
$"未找到与 \"{_searchText}\" 匹配的房间", _noResultStyle);
|
||||
}
|
||||
|
||||
GUI.EndClip();
|
||||
|
||||
// R9-N4 图例面板(独立绘制,不参与裁剪)
|
||||
if (_showLegend)
|
||||
DrawLegendPanel(mapRect);
|
||||
}
|
||||
|
||||
private void DrawPlayModePlayerDot(Vector2 origin)
|
||||
{
|
||||
var provider = BaseGames.Core.ServiceLocator.GetOrDefault<IPlayerPositionProvider>();
|
||||
if (provider == null) return;
|
||||
var roomId = provider.CurrentRoomId;
|
||||
if (string.IsNullOrEmpty(roomId)) return;
|
||||
var room = _database.GetRoom(roomId);
|
||||
if (room == null) return;
|
||||
|
||||
Rect cell = RoomToClipRect(room, origin);
|
||||
Vector2 norm = provider.NormalizedPositionInRoom;
|
||||
// norm.y=0 是房间底部;屏幕 Y 向下需翻转
|
||||
float px = cell.x + norm.x * cell.width;
|
||||
float py = cell.y + (1f - norm.y) * cell.height;
|
||||
float r = Mathf.Max(4f, _zoom * 0.18f);
|
||||
var prev = GUI.color;
|
||||
GUI.color = new Color(1f, 0.2f, 0.2f, 0.95f);
|
||||
GUI.DrawTexture(new Rect(px - r, py - r, r * 2f, r * 2f), EditorGUIUtility.whiteTexture);
|
||||
GUI.color = Color.white;
|
||||
DrawBorder(new Rect(px - r - 1f, py - r - 1f, r * 2f + 2f, r * 2f + 2f), Color.white, 1f);
|
||||
GUI.color = prev;
|
||||
|
||||
// 持续刷新使 Play Mode 玩家位置可视化跟随移动
|
||||
Repaint();
|
||||
}
|
||||
|
||||
private void DrawLegendPanel(Rect mapRect)
|
||||
{
|
||||
const float W = 200f;
|
||||
float h = 26f + _regionColors.Count * 18f + (Application.isPlaying ? 18f : 0f);
|
||||
var panel = new Rect(mapRect.xMax - W - 8f, mapRect.y + 8f, W, h);
|
||||
EditorGUI.DrawRect(panel, new Color(0.12f, 0.12f, 0.12f, 0.85f));
|
||||
DrawBorder(panel, new Color(0.4f, 0.4f, 0.4f, 1f), 1f);
|
||||
|
||||
var labelStyle = EditorStyles.miniBoldLabel;
|
||||
GUI.Label(new Rect(panel.x + 6f, panel.y + 4f, W - 12f, 16f), "Region 图例", labelStyle);
|
||||
|
||||
float y = panel.y + 22f;
|
||||
foreach (var kv in _regionColors)
|
||||
{
|
||||
EditorGUI.DrawRect(new Rect(panel.x + 8f, y + 3f, 12f, 12f), kv.Value);
|
||||
GUI.Label(new Rect(panel.x + 24f, y, W - 30f, 16f),
|
||||
string.IsNullOrEmpty(kv.Key) ? "(无 Region)" : kv.Key, EditorStyles.miniLabel);
|
||||
y += 18f;
|
||||
}
|
||||
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
EditorGUI.DrawRect(new Rect(panel.x + 8f, y + 4f, 10f, 10f), new Color(1f, 0.2f, 0.2f));
|
||||
GUI.Label(new Rect(panel.x + 24f, y, W - 30f, 16f), "玩家位置(Play Mode)", EditorStyles.miniLabel);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -235,6 +443,17 @@ namespace BaseGames.Editor.Map
|
||||
/// </summary>
|
||||
private void EnsureLabelStyles()
|
||||
{
|
||||
// _noResultStyle 不依赖 zoom,初始化一次即可
|
||||
if (_noResultStyle == null)
|
||||
{
|
||||
_noResultStyle = new GUIStyle(EditorStyles.boldLabel)
|
||||
{
|
||||
alignment = TextAnchor.MiddleCenter,
|
||||
normal = { textColor = new Color(1f, 0.8f, 0.2f, 0.8f) },
|
||||
fontSize = 13,
|
||||
};
|
||||
}
|
||||
|
||||
if (Mathf.Approximately(_cachedZoomForStyle, _zoom) && _roomLabelStyle != null) return;
|
||||
|
||||
_cachedZoomForStyle = _zoom;
|
||||
@@ -267,18 +486,27 @@ namespace BaseGames.Editor.Map
|
||||
|
||||
private void DrawRoomBadge(Rect cell, MapRoomDataSO room)
|
||||
{
|
||||
if (!room.IsBossRoom && !room.IsSavePoint && !room.IsShop) return;
|
||||
// R12-N9 优先检查 RoomFlags(新 [Flags] 枚举),回退兼容旧 bool 字段
|
||||
// R15-N1 补充 TeleportStation,与运行时 MapRoomDataSO.ChooseDisplayIcon 保持一致
|
||||
bool isBoss = room.RoomFlags.HasFlag(RoomType.BossRoom) || room.IsBossRoom;
|
||||
bool isSave = room.RoomFlags.HasFlag(RoomType.SavePoint) || room.IsSavePoint;
|
||||
bool isShop = room.RoomFlags.HasFlag(RoomType.Shop) || room.IsShop;
|
||||
bool isTeleport = room.RoomFlags.HasFlag(RoomType.TeleportStation);
|
||||
if (!isBoss && !isSave && !isShop && !isTeleport) return;
|
||||
if (cell.width < 8f) return;
|
||||
|
||||
string badge = room.IsBossRoom ? "★" :
|
||||
room.IsSavePoint ? "♦" : "¥";
|
||||
GUI.Label(cell, badge, room.IsBossRoom ? _badgeBossStyle : _badgeNormalStyle);
|
||||
// 优先级与运行时 MapRoomDataSO.ChooseDisplayIcon 对齐:Save > Boss > Shop > Teleport
|
||||
string badge = isSave ? "♦" : isBoss ? "★" : isShop ? "¥" : "⇅";
|
||||
// 徽章样式与实际显示的徽章类型对齐(不随 isBoss 变化,而是随 badge 内容)
|
||||
GUI.Label(cell, badge, badge == "★" ? _badgeBossStyle : _badgeNormalStyle);
|
||||
}
|
||||
|
||||
private void DrawExitLines(Vector2 origin)
|
||||
{
|
||||
if (_database?.AllRooms == null) return;
|
||||
var lineColor = new Color(1f, 1f, 0.5f, 0.35f);
|
||||
// R12-N2 去重:A→B 和 B→A 各画一次,用规范化有序键防止重复绘制
|
||||
// N4: 复用字段级 HashSet,避免每次 OnGUI 分配新对象
|
||||
_drawnExitPairs.Clear();
|
||||
|
||||
foreach (var room in _database.AllRooms)
|
||||
{
|
||||
@@ -288,10 +516,34 @@ namespace BaseGames.Editor.Map
|
||||
var target = _database.GetRoom(exit.TargetRoomId);
|
||||
if (target == null) continue;
|
||||
|
||||
Vector2 from = GridCenterToClip(room.GridPosition + room.GridSize / 2, origin);
|
||||
Vector2 to = GridCenterToClip(target.GridPosition + target.GridSize / 2, origin);
|
||||
// R12-N2 规范化键:按 Ordinal 比较,较小 ID 在前,避免 A-B / B-A 重复画线
|
||||
var keyA = string.CompareOrdinal(room.RoomId, exit.TargetRoomId) <= 0
|
||||
? (room.RoomId, exit.TargetRoomId)
|
||||
: (exit.TargetRoomId, room.RoomId);
|
||||
if (!_drawnExitPairs.Add(keyA)) continue;
|
||||
|
||||
DrawLine(from, to, lineColor, 1.5f);
|
||||
// R12-N5 使用 HasCustomExitPos 替代 != Vector2Int.zero 哨兵,避免合法原点坐标歧义
|
||||
Vector2Int fromGrid = exit.HasCustomExitPos
|
||||
? exit.ExitGridPos
|
||||
: room.GridPosition + room.GridSize / 2;
|
||||
|
||||
// 找目标房间中与本出口对应的反向出口(方向相反且 TargetRoomId == room.RoomId)
|
||||
Vector2Int toGrid = target.GridPosition + target.GridSize / 2; // fallback
|
||||
if (target.Exits != null)
|
||||
{
|
||||
foreach (var rev in target.Exits)
|
||||
{
|
||||
if (rev.TargetRoomId == room.RoomId && rev.HasCustomExitPos)
|
||||
{
|
||||
toGrid = rev.ExitGridPos;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vector2 from = GridCenterToClip(fromGrid, origin);
|
||||
Vector2 to = GridCenterToClip(toGrid, origin);
|
||||
DrawLine(from, to, ExitLineColor, 1.5f);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -311,30 +563,8 @@ namespace BaseGames.Editor.Map
|
||||
}
|
||||
|
||||
// ── 房间选择 ──────────────────────────────────────────────────────────
|
||||
|
||||
private void TrySelectRoom(Vector2 clipPos, Rect mapRect)
|
||||
{
|
||||
if (_database?.AllRooms == null) return;
|
||||
|
||||
// 使用与 DrawMapArea 完全相同的 origin 公式,确保命中测试一致
|
||||
Vector2 origin = mapRect.size * 0.5f + _panOffset;
|
||||
|
||||
foreach (var room in _database.AllRooms)
|
||||
{
|
||||
if (room == null) continue;
|
||||
Rect cell = RoomToClipRect(room, origin);
|
||||
if (cell.Contains(clipPos))
|
||||
{
|
||||
_selectedRoom = room;
|
||||
Selection.activeObject = room;
|
||||
EditorGUIUtility.PingObject(room);
|
||||
Repaint();
|
||||
return;
|
||||
}
|
||||
}
|
||||
_selectedRoom = null;
|
||||
Repaint();
|
||||
}
|
||||
// R9-N2 后由 HandleInput 中的 MouseDown 直接调用 HitTestRoom 完成选中+起拖,
|
||||
// 旧的 TrySelectRoom(仅在 MouseUp 时选中)已合并到 HandleInput。
|
||||
|
||||
// ── 工具方法 ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -361,6 +591,23 @@ namespace BaseGames.Editor.Map
|
||||
return c;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R23-N2 检查房间的 RoomFlags 是否包含与 searchText 大小写不敏感匹配的 RoomType 枚举成员。
|
||||
/// 允许策划通过搜索 "SavePoint"、"BossRoom" 等关键字快速过滤特定类型的房间。
|
||||
/// </summary>
|
||||
private static bool MatchesRoomType(RoomType flags, string searchText)
|
||||
{
|
||||
if (flags == RoomType.None) return false;
|
||||
foreach (RoomType value in System.Enum.GetValues(typeof(RoomType)))
|
||||
{
|
||||
if (value == RoomType.None) continue;
|
||||
if (!flags.HasFlag(value)) continue;
|
||||
if (value.ToString().IndexOf(searchText, System.StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void RunValidation()
|
||||
{
|
||||
_validationErrors = _database?.ValidateAll() ?? new List<string>();
|
||||
@@ -399,11 +646,18 @@ namespace BaseGames.Editor.Map
|
||||
var prevMatrix = GUI.matrix;
|
||||
var prevColor = GUI.color;
|
||||
GUI.color = color;
|
||||
GUIUtility.RotateAroundPivot(angle, mid);
|
||||
GUI.DrawTexture(new Rect(mid.x - len * 0.5f, mid.y - thickness * 0.5f, len, thickness),
|
||||
EditorGUIUtility.whiteTexture);
|
||||
GUI.matrix = prevMatrix;
|
||||
GUI.color = prevColor;
|
||||
// R11-N12 try/finally 保证 GUI.matrix/color 在 DrawTexture 抛出异常时仍能恢复
|
||||
try
|
||||
{
|
||||
GUIUtility.RotateAroundPivot(angle, mid);
|
||||
GUI.DrawTexture(new Rect(mid.x - len * 0.5f, mid.y - thickness * 0.5f, len, thickness),
|
||||
EditorGUIUtility.whiteTexture);
|
||||
}
|
||||
finally
|
||||
{
|
||||
GUI.matrix = prevMatrix;
|
||||
GUI.color = prevColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
105
Assets/_Game/Scripts/Editor/World/Map/MapRoomAutoRegister.cs
Normal file
105
Assets/_Game/Scripts/Editor/World/Map/MapRoomAutoRegister.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
#if UNITY_EDITOR
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using BaseGames.World.Map;
|
||||
|
||||
namespace BaseGames.Editor.Map
|
||||
{
|
||||
/// <summary>
|
||||
/// 新建 MapRoomDataSO 时自动追加到默认 MapDatabaseSO 的 AllRooms 列表(R9-N3)。
|
||||
/// 解决问题:开发/策划新建房间后忘记手动拖入 Database,导致运行时小地图缺失房间。
|
||||
/// <para>
|
||||
/// 触发条件:检测到导入的 MapRoomDataSO 资产路径中包含项目内任意 Database 引用即跳过;
|
||||
/// 若不被任何 Database 引用,则追加到"默认"Database(首个被找到的 MapDatabaseSO,
|
||||
/// 按 AssetDatabase.FindAssets GUID 排序保证可复现)。
|
||||
/// </para>
|
||||
/// 不会修改资产文件层级结构;仅修改 Database 的 AllRooms 数组并 SetDirty。
|
||||
/// 可在 EditorPrefs 中通过 Key <see cref="DISABLE_KEY"/> 设为 true 来禁用。
|
||||
/// </summary>
|
||||
public class MapRoomAutoRegister : AssetPostprocessor
|
||||
{
|
||||
private const string DISABLE_KEY = "BaseGames.Map.AutoRegister.Disabled";
|
||||
|
||||
private static void OnPostprocessAllAssets(
|
||||
string[] importedAssets,
|
||||
string[] deletedAssets,
|
||||
string[] movedAssets,
|
||||
string[] movedFromAssetPaths)
|
||||
{
|
||||
if (EditorPrefs.GetBool(DISABLE_KEY, false)) return;
|
||||
if (importedAssets == null || importedAssets.Length == 0) return;
|
||||
|
||||
// 仅处理新导入的 MapRoomDataSO(含 .asset 后缀过滤,减少 LoadAssetAtPath 调用)
|
||||
List<MapRoomDataSO> newRooms = null;
|
||||
foreach (var path in importedAssets)
|
||||
{
|
||||
if (!path.EndsWith(".asset", System.StringComparison.OrdinalIgnoreCase)) continue;
|
||||
var room = AssetDatabase.LoadAssetAtPath<MapRoomDataSO>(path);
|
||||
if (room == null) continue;
|
||||
(newRooms ??= new List<MapRoomDataSO>()).Add(room);
|
||||
}
|
||||
if (newRooms == null) return;
|
||||
|
||||
// 查找项目内所有 MapDatabaseSO(按 GUID 排序确保"默认 Database"在多人协作下一致)
|
||||
var dbGuids = AssetDatabase.FindAssets($"t:{nameof(MapDatabaseSO)}");
|
||||
if (dbGuids.Length == 0) return;
|
||||
var databases = dbGuids
|
||||
.OrderBy(g => g, System.StringComparer.Ordinal)
|
||||
.Select(g => AssetDatabase.LoadAssetAtPath<MapDatabaseSO>(AssetDatabase.GUIDToAssetPath(g)))
|
||||
.Where(db => db != null)
|
||||
.ToList();
|
||||
if (databases.Count == 0) return;
|
||||
|
||||
// R10-N4 默认 Database 选择优先级:
|
||||
// 1) 显式勾选 IsDefault 的首个(GUID 排序);
|
||||
// 2) 否则回退 GUID 排序首个。
|
||||
var defaultDb = databases.FirstOrDefault(db => db.IsDefault) ?? databases[0];
|
||||
bool dirty = false;
|
||||
|
||||
foreach (var room in newRooms)
|
||||
{
|
||||
// 已被任意 Database 引用则跳过(避免重复追加 + 跨 Database 误抢占)
|
||||
if (databases.Any(db => db.AllRooms != null && System.Array.IndexOf(db.AllRooms, room) >= 0))
|
||||
continue;
|
||||
|
||||
var arr = defaultDb.AllRooms ?? System.Array.Empty<MapRoomDataSO>();
|
||||
var newArr = new MapRoomDataSO[arr.Length + 1];
|
||||
System.Array.Copy(arr, newArr, arr.Length);
|
||||
newArr[arr.Length] = room;
|
||||
defaultDb.EditorSetRooms(newArr);
|
||||
dirty = true;
|
||||
|
||||
Debug.Log($"[MapRoomAutoRegister] 已将新房间 '{room.RoomId}' 自动注册到 Database '{defaultDb.name}'。", defaultDb);
|
||||
}
|
||||
|
||||
if (dirty)
|
||||
{
|
||||
EditorUtility.SetDirty(defaultDb);
|
||||
// 不主动 SaveAssets:避免在批量导入流程中拖慢 IDE;下次工程保存自动落盘
|
||||
}
|
||||
|
||||
// R12-N4 清理所有 Database 中的 null 条目(资产被删除后留下的空引用)
|
||||
foreach (var db in databases)
|
||||
{
|
||||
if (db.AllRooms == null) continue;
|
||||
if (!System.Array.Exists(db.AllRooms, r => r == null)) continue;
|
||||
var cleaned = System.Array.FindAll(db.AllRooms, r => r != null);
|
||||
int nullCount = db.AllRooms.Length - cleaned.Length;
|
||||
db.EditorSetRooms(cleaned);
|
||||
EditorUtility.SetDirty(db);
|
||||
Debug.Log($"[MapRoomAutoRegister] 已清理 Database '{db.name}' 中的 {nullCount} 个 null 条目。", db);
|
||||
}
|
||||
}
|
||||
|
||||
[MenuItem("Tools/Map/Toggle Auto-Register New Rooms")]
|
||||
private static void ToggleAutoRegister()
|
||||
{
|
||||
bool disabled = EditorPrefs.GetBool(DISABLE_KEY, false);
|
||||
EditorPrefs.SetBool(DISABLE_KEY, !disabled);
|
||||
Debug.Log($"[MapRoomAutoRegister] 自动注册已 {(!disabled ? "禁用" : "启用")}。");
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 770823afe767eef48a373ddbbb7eeaa0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -11,6 +11,7 @@ namespace BaseGames.Editor.Map
|
||||
/// 拖动自动吸附到整格精度;左下/右上角可独立拖动(含反转保护);支持 Undo。
|
||||
/// </summary>
|
||||
[CustomEditor(typeof(MapRoomDataSO))]
|
||||
[CanEditMultipleObjects]
|
||||
public class MapRoomDataEditor : UnityEditor.Editor
|
||||
{
|
||||
private const float CELL_SIZE = 1f; // 每格在 Scene 中的世界单位尺寸
|
||||
@@ -19,13 +20,23 @@ namespace BaseGames.Editor.Map
|
||||
private static readonly Color OutlineColor = new Color(0.2f, 0.6f, 1f, 0.9f);
|
||||
private static readonly Color HandleColor = new Color(1f, 0.85f, 0.2f, 1f);
|
||||
|
||||
private static readonly GUIStyle LabelStyle = new GUIStyle
|
||||
// 静态 GUIStyle 在域重载/字段初始化器顺序下可能产生 NRE(normal.background=null 访问)。
|
||||
// 改用懒加载 + 首次访问时构建,避免 EditorStyles 未初始化导致的访问问题。
|
||||
private static GUIStyle s_labelStyle;
|
||||
private static GUIStyle s_cornerLabelStyle;
|
||||
|
||||
private static GUIStyle LabelStyle => s_labelStyle ??= new GUIStyle(EditorStyles.boldLabel)
|
||||
{
|
||||
alignment = TextAnchor.MiddleCenter,
|
||||
fontStyle = FontStyle.Bold,
|
||||
normal = { textColor = Color.white },
|
||||
};
|
||||
|
||||
private static GUIStyle CornerLabelStyle => s_cornerLabelStyle ??= new GUIStyle(EditorStyles.miniLabel)
|
||||
{
|
||||
alignment = TextAnchor.MiddleLeft,
|
||||
normal = { textColor = new Color(1f, 0.85f, 0.2f, 1f) },
|
||||
};
|
||||
|
||||
private MapRoomDataSO _target;
|
||||
|
||||
private void OnEnable() => _target = (MapRoomDataSO)target;
|
||||
@@ -101,6 +112,8 @@ namespace BaseGames.Editor.Map
|
||||
Color prev = Handles.color;
|
||||
Handles.color = HandleColor;
|
||||
var result = Handles.FreeMoveHandle(pos, sz, Vector3.zero, Handles.DotHandleCap);
|
||||
// 角点标签:标识 BL / TR 角,便于多房间布局编辑时区分;偏移避免遮挡 handle
|
||||
Handles.Label(pos + new Vector3(sz * 1.5f, sz * 0.5f, 0f), label, CornerLabelStyle);
|
||||
Handles.color = prev;
|
||||
return SnapToGrid(result);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user