Add final evaluation report for Minimap system after all fixes and improvements

- Summarized the evolution of scores across five review rounds
- Detailed the status of each evaluation dimension post-fixes
- Highlighted remaining issues and recommended future work for further enhancements
- Compared current system against industry benchmarks
This commit is contained in:
2026-05-25 14:25:19 +08:00
parent a1f9122153
commit 5cb6c2a19d
64 changed files with 2358 additions and 32937 deletions

View File

@@ -0,0 +1,148 @@
#if UNITY_EDITOR
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using BaseGames.World.Map;
namespace BaseGames.Editor.Map
{
/// <summary>
/// MapDatabaseSO 的自定义 Inspector
/// <list type="bullet">
/// <item>显示房间总数、已配置出口数</item>
/// <item>一键 "重新验证",将错误以 HelpBox 列出</item>
/// <item>一键 "打开布局编辑器" 跳转到 MapLayoutEditorWindow</item>
/// <item>可展开的房间列表,每行点击可 Ping 对应 SO</item>
/// </list>
/// </summary>
[CustomEditor(typeof(MapDatabaseSO))]
public class MapDatabaseEditor : UnityEditor.Editor
{
private MapDatabaseSO _database;
private List<string> _lastErrors;
private bool _roomListExpanded;
private Vector2 _roomListScroll;
/// <summary>
/// 缓存的错误房间 ID 集合,仅在"重新验证"按钮点击时重建。
/// 避免 OnInspectorGUI高频重绘每次都重建 O(N²) 扫描。
/// </summary>
private readonly HashSet<string> _cachedErrorRoomIds = new();
private static readonly GUIContent LabelValidate = new GUIContent("重新验证", "检查重复 RoomId、出口目标缺失、房间网格重叠等问题");
private static readonly GUIContent LabelOpenEditor = new GUIContent("打开布局编辑器", "在独立窗口中预览全局地图布局");
private void OnEnable() => _database = (MapDatabaseSO)target;
public override void OnInspectorGUI()
{
serializedObject.Update();
// ── 统计摘要 ──────────────────────────────────────────────────────
int roomCount = _database.AllRooms?.Length ?? 0;
int exitCount = 0;
if (_database.AllRooms != null)
foreach (var r in _database.AllRooms)
if (r?.Exits != null) exitCount += r.Exits.Length;
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.LabelField("📊 数据库统计", EditorStyles.boldLabel);
EditorGUI.indentLevel++;
EditorGUILayout.LabelField("房间总数", roomCount.ToString());
EditorGUILayout.LabelField("出口总数", exitCount.ToString());
EditorGUI.indentLevel--;
EditorGUILayout.EndVertical();
EditorGUILayout.Space(4);
// ── 操作按钮 ──────────────────────────────────────────────────────
EditorGUILayout.BeginHorizontal();
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);
if (_lastErrors.Count == 0)
Debug.Log("[MapDatabase] 验证通过,未发现问题 ✓");
else
Debug.LogWarning($"[MapDatabase] 发现 {_lastErrors.Count} 项问题,详见 Inspector。");
}
if (GUILayout.Button(LabelOpenEditor, GUILayout.Height(28)))
{
var win = EditorWindow.GetWindow<MapLayoutEditorWindow>("地图布局编辑器");
win.SetDatabase(_database);
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(4);
// ── 验证结果 ──────────────────────────────────────────────────────
if (_lastErrors != null)
{
if (_lastErrors.Count == 0)
{
EditorGUILayout.HelpBox("验证通过,无问题 ✓", MessageType.Info);
}
else
{
string errorText = string.Join("\n", _lastErrors);
EditorGUILayout.HelpBox(errorText, MessageType.Warning);
}
}
EditorGUILayout.Space(4);
// ── 默认字段AllRooms 数组等) ───────────────────────────────────
DrawDefaultInspector();
EditorGUILayout.Space(4);
// ── 可折叠房间列表(快速总览) ────────────────────────────────────
_roomListExpanded = EditorGUILayout.Foldout(_roomListExpanded, $"房间列表({roomCount}", true);
if (_roomListExpanded && _database.AllRooms != null)
{
_roomListScroll = EditorGUILayout.BeginScrollView(_roomListScroll, GUILayout.MaxHeight(200));
foreach (var room in _database.AllRooms)
{
if (room == null)
{
EditorGUILayout.HelpBox("空引用AllRooms 中有 null 元素)", MessageType.Error);
continue;
}
bool hasError = _cachedErrorRoomIds.Contains(room.RoomId);
var rowStyle = hasError
? new GUIStyle(EditorStyles.label) { normal = { textColor = new Color(1f, 0.35f, 0.35f) } }
: EditorStyles.label;
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField(
$"{(hasError ? " " : "")}{room.RoomId} [{room.GridPosition.x},{room.GridPosition.y}] {room.GridSize.x}×{room.GridSize.y}",
rowStyle);
if (GUILayout.Button("Ping", EditorStyles.miniButton, GUILayout.Width(40)))
{
Selection.activeObject = room;
EditorGUIUtility.PingObject(room);
}
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndScrollView();
}
serializedObject.ApplyModifiedProperties();
}
}
}
#endif

View File

@@ -0,0 +1,391 @@
#if UNITY_EDITOR
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using BaseGames.World.Map;
namespace BaseGames.Editor.Map
{
/// <summary>
/// 全局地图布局预览编辑器窗口(架构 15_MapShopModule §5.2)。
/// 菜单BaseGames/Map/Map Layout Editor
/// <list type="bullet">
/// <item>以格子坐标显示 MapDatabaseSO 中所有房间</item>
/// <item>鼠标滚轮缩放,中键(或 Alt+左键)拖拽平移</item>
/// <item>按区域自动着色,红色高亮配置错误(重叠/重复)</item>
/// <item>左键点击房间Selection + Ping 对应 SO并在 Inspector 中打开</item>
/// </list>
/// </summary>
public class MapLayoutEditorWindow : EditorWindow
{
[MenuItem("BaseGames/Map/Map Layout Editor", priority = 100)]
public static void ShowWindow() => GetWindow<MapLayoutEditorWindow>("地图布局编辑器");
// ── 状态 ──────────────────────────────────────────────────────────────
private MapDatabaseSO _database;
private Vector2 _panOffset;
private float _zoom = 24f; // px / 格
private bool _isDragging;
private Vector2 _dragStart;
private MapRoomDataSO _selectedRoom;
private List<string> _validationErrors;
private HashSet<string> _errorRoomIds;
/// <summary>缓存的错误房间集合,由验证按钮点击时重建(防止 OnInspectorGUI 高频重建导致 GC。</summary>
private readonly HashSet<string> _cachedErrorRoomIds = new();
private readonly Dictionary<string, Color> _regionColors = new();
private int _paletteIndex;
// 缓存 GUIStyle避免每帧每格重新分配约 100 房间 × 60fps = 大量 GC 压力)
private GUIStyle _roomLabelStyle;
private GUIStyle _badgeBossStyle;
private GUIStyle _badgeNormalStyle;
private float _cachedZoomForStyle = -1f; // 用于检测缩放变化时重建 Style
// 区域配色方案(与 MapExploration 标注颜色视觉呼应)
private static readonly Color[] Palette =
{
new Color(0.30f, 0.60f, 1.00f),
new Color(0.30f, 1.00f, 0.55f),
new Color(1.00f, 0.60f, 0.25f),
new Color(0.85f, 0.30f, 0.90f),
new Color(1.00f, 0.88f, 0.20f),
new Color(0.25f, 0.90f, 1.00f),
new Color(1.00f, 0.45f, 0.45f),
new Color(0.50f, 1.00f, 0.80f),
};
// ── 主 GUI ────────────────────────────────────────────────────────────
private void OnGUI()
{
DrawToolbar();
if (_database == null)
{
EditorGUILayout.HelpBox("请在上方选择 MapDatabaseSO 资产以预览地图布局。", MessageType.Info);
return;
}
// 验证错误摘要
if (_validationErrors != null && _validationErrors.Count > 0)
{
int shown = Mathf.Min(3, _validationErrors.Count);
string msg = string.Join("\n", _validationErrors.GetRange(0, shown));
if (_validationErrors.Count > shown) msg += $"\n…共 {_validationErrors.Count} 项)";
EditorGUILayout.HelpBox(msg, MessageType.Warning);
}
// 地图绘制区域
Rect mapRect = GUILayoutUtility.GetRect(
GUIContent.none, GUIStyle.none,
GUILayout.ExpandWidth(true), GUILayout.ExpandHeight(true));
HandleInput(mapRect);
DrawMapArea(mapRect);
}
// ── 工具栏 ────────────────────────────────────────────────────────────
private void DrawToolbar()
{
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
var newDb = (MapDatabaseSO)EditorGUILayout.ObjectField(
_database, typeof(MapDatabaseSO), false, GUILayout.Width(240));
if (newDb != _database)
{
_database = newDb;
_validationErrors = null;
_errorRoomIds = null;
_regionColors.Clear();
_paletteIndex = 0;
_selectedRoom = null;
Repaint();
}
GUILayout.FlexibleSpace();
if (GUILayout.Button("验 证", EditorStyles.toolbarButton, GUILayout.Width(60)))
RunValidation();
if (GUILayout.Button("重置视图", EditorStyles.toolbarButton, GUILayout.Width(72)))
{
_zoom = 24f;
_panOffset = Vector2.zero;
Repaint();
}
EditorGUILayout.LabelField($"缩放 {_zoom:F0}px/格", GUILayout.Width(80));
EditorGUILayout.EndHorizontal();
}
// ── 输入处理 ──────────────────────────────────────────────────────────
private void HandleInput(Rect mapRect)
{
var e = Event.current;
if (!mapRect.Contains(e.mousePosition)) return;
switch (e.type)
{
case EventType.ScrollWheel:
_zoom = Mathf.Clamp(_zoom - e.delta.y * 1.8f, 6f, 96f);
e.Use();
Repaint();
break;
// 中键拖拽 或 Alt+左键拖拽 平移
case EventType.MouseDown when e.button == 2 || (e.button == 0 && e.alt):
_isDragging = true;
_dragStart = e.mousePosition;
e.Use();
break;
case EventType.MouseDrag when _isDragging:
_panOffset += e.mousePosition - _dragStart;
_dragStart = e.mousePosition;
e.Use();
Repaint();
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);
break;
}
}
// ── 地图绘制 ──────────────────────────────────────────────────────────
private void DrawMapArea(Rect mapRect)
{
if (_database?.AllRooms == null) return;
EnsureLabelStyles();
GUI.BeginClip(mapRect);
Vector2 origin = mapRect.size * 0.5f + _panOffset;
foreach (var room in _database.AllRooms)
{
if (room == null) continue;
Rect cell = RoomToClipRect(room, origin);
// 填充
bool hasError = _errorRoomIds != null && _errorRoomIds.Contains(room.RoomId);
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);
EditorGUI.DrawRect(cell, fillColor);
// 边框
Color borderColor = room == _selectedRoom
? new Color(1f, 0.9f, 0.1f, 1f)
: new Color(regionColor.r, regionColor.g, regionColor.b, 0.85f);
float border = room == _selectedRoom ? 2f : 1f;
DrawBorder(cell, borderColor, border);
// 标签(缩放足够大时才显示,使用缓存 Style
if (_zoom >= 18f)
GUI.Label(cell, string.IsNullOrEmpty(room.RoomId) ? "?" : room.RoomId, _roomLabelStyle);
// 特殊图标标记Boss / 存档点 / 商店)
DrawRoomBadge(cell, room);
}
// 出口连线(缩放足够大时)
if (_zoom >= 12f)
DrawExitLines(origin);
GUI.EndClip();
}
/// <summary>
/// 按需重建 GUIStyle 缓存——仅当 zoom 发生变化(或首次调用)时重建,
/// 避免每帧每格 <c>new GUIStyle(...)</c> 造成大量 GC 分配。
/// </summary>
private void EnsureLabelStyles()
{
if (Mathf.Approximately(_cachedZoomForStyle, _zoom) && _roomLabelStyle != null) return;
_cachedZoomForStyle = _zoom;
int fontSize = Mathf.Clamp(Mathf.RoundToInt(_zoom * 0.4f), 8, 14);
_roomLabelStyle = new GUIStyle(EditorStyles.miniLabel)
{
alignment = TextAnchor.MiddleCenter,
wordWrap = false,
normal = { textColor = Color.white },
fontSize = fontSize,
};
int badgeSize = Mathf.Max(8, Mathf.RoundToInt(_zoom * 0.35f));
_badgeBossStyle = new GUIStyle(EditorStyles.miniLabel)
{
alignment = TextAnchor.UpperRight,
normal = { textColor = new Color(1f, 0.4f, 0.4f) },
fontSize = badgeSize,
};
_badgeNormalStyle = new GUIStyle(EditorStyles.miniLabel)
{
alignment = TextAnchor.UpperRight,
normal = { textColor = Color.cyan },
fontSize = badgeSize,
};
}
private void DrawRoomBadge(Rect cell, MapRoomDataSO room)
{
if (!room.IsBossRoom && !room.IsSavePoint && !room.IsShop) return;
if (cell.width < 8f) return;
string badge = room.IsBossRoom ? "★" :
room.IsSavePoint ? "♦" : "¥";
GUI.Label(cell, badge, room.IsBossRoom ? _badgeBossStyle : _badgeNormalStyle);
}
private void DrawExitLines(Vector2 origin)
{
if (_database?.AllRooms == null) return;
var lineColor = new Color(1f, 1f, 0.5f, 0.35f);
foreach (var room in _database.AllRooms)
{
if (room?.Exits == null) continue;
foreach (var exit in room.Exits)
{
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);
DrawLine(from, to, lineColor, 1.5f);
}
}
}
// ── 公共 API ──────────────────────────────────────────────────────────
/// <summary>由外部(如 MapDatabaseEditor注入 Database避免反射访问私有字段。</summary>
public void SetDatabase(MapDatabaseSO db)
{
_database = db;
_validationErrors = null;
_errorRoomIds = null;
_regionColors.Clear();
_paletteIndex = 0;
_selectedRoom = null;
Repaint();
}
// ── 房间选择 ──────────────────────────────────────────────────────────
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();
}
// ── 工具方法 ──────────────────────────────────────────────────────────
private Rect RoomToClipRect(MapRoomDataSO room, Vector2 origin)
{
float x = origin.x + room.GridPosition.x * _zoom;
// Y 轴:格子坐标 Y 向上,屏幕 Y 向下,需翻转
float y = origin.y - (room.GridPosition.y + room.GridSize.y) * _zoom;
return new Rect(x, y, room.GridSize.x * _zoom, room.GridSize.y * _zoom);
}
private Vector2 GridCenterToClip(Vector2Int gridPos, Vector2 origin)
=> new Vector2(origin.x + gridPos.x * _zoom, origin.y - gridPos.y * _zoom);
private Color GetRegionColor(string regionId)
{
if (string.IsNullOrEmpty(regionId)) return new Color(0.55f, 0.55f, 0.55f);
if (!_regionColors.TryGetValue(regionId, out var c))
{
c = Palette[_paletteIndex % Palette.Length];
_paletteIndex++;
_regionColors[regionId] = c;
}
return c;
}
private void RunValidation()
{
_validationErrors = _database?.ValidateAll() ?? new List<string>();
_errorRoomIds = new HashSet<string>();
if (_validationErrors != null && _database?.AllRooms != null)
foreach (var err in _validationErrors)
foreach (var r in _database.AllRooms)
if (r != null && err.Contains($"'{r.RoomId}'"))
_errorRoomIds.Add(r.RoomId);
Repaint();
}
// ── 绘图辅助 ──────────────────────────────────────────────────────────
private static void DrawBorder(Rect r, Color color, float t)
{
var prev = GUI.color;
GUI.color = color;
GUI.DrawTexture(new Rect(r.x, r.y, r.width, t), EditorGUIUtility.whiteTexture);
GUI.DrawTexture(new Rect(r.x, r.yMax - t, r.width, t), EditorGUIUtility.whiteTexture);
GUI.DrawTexture(new Rect(r.x, r.y, t, r.height), EditorGUIUtility.whiteTexture);
GUI.DrawTexture(new Rect(r.xMax - t, r.y, t, r.height), EditorGUIUtility.whiteTexture);
GUI.color = prev;
}
// 用细矩形近似绘制连线(无需 GL/Handles
private static void DrawLine(Vector2 a, Vector2 b, Color color, float thickness)
{
Vector2 dir = b - a;
float len = dir.magnitude;
if (len < 1f) return;
float angle = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg;
Vector2 mid = (a + b) * 0.5f;
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;
}
}
}
#endif