Add independent review report for Minimap system Round 7

- Validate fixes from Round 6 and identify new issues
- Document findings including UX defects, editor integration flaws, and code quality concerns
- Propose solutions and prioritize issues based on severity
- Evaluate against standards of mature 2D Metroidvania games
This commit is contained in:
2026-05-25 14:44:31 +08:00
parent 5cb6c2a19d
commit e2bc324905
17 changed files with 1060 additions and 17 deletions

View File

@@ -29,10 +29,26 @@ namespace BaseGames.Editor.Map
/// </summary>
private readonly HashSet<string> _cachedErrorRoomIds = new();
/// <summary>错误行文本颜色样式,惰性初始化后复用,避免 OnInspectorGUI 每帧分配。</summary>
private GUIStyle _errorRowStyle;
private static readonly GUIContent LabelValidate = new GUIContent("重新验证", "检查重复 RoomId、出口目标缺失、房间网格重叠等问题");
private static readonly GUIContent LabelOpenEditor = new GUIContent("打开布局编辑器", "在独立窗口中预览全局地图布局");
private void OnEnable() => _database = (MapDatabaseSO)target;
private void OnEnable()
{
_database = (MapDatabaseSO)target;
_errorRowStyle = null; // 编辑器皮肤切换时(亮/暗模式)需重建
}
/// <summary>错误行 GUIStyle 惰性初始化,基于当前编辑器皮肤构建,避免 OnInspectorGUI 每帧分配。</summary>
private GUIStyle GetErrorRowStyle()
{
if (_errorRowStyle == null)
_errorRowStyle = new GUIStyle(EditorStyles.label)
{ normal = { textColor = new Color(1f, 0.35f, 0.35f) } };
return _errorRowStyle;
}
public override void OnInspectorGUI()
{
@@ -122,9 +138,7 @@ namespace BaseGames.Editor.Map
}
bool hasError = _cachedErrorRoomIds.Contains(room.RoomId);
var rowStyle = hasError
? new GUIStyle(EditorStyles.label) { normal = { textColor = new Color(1f, 0.35f, 0.35f) } }
: EditorStyles.label;
var rowStyle = hasError ? GetErrorRowStyle() : EditorStyles.label;
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField(

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e42e54d73570d0245b6bb4e722c3b0f8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -59,6 +59,25 @@ namespace BaseGames.Editor.Map
// ── 主 GUI ────────────────────────────────────────────────────────────
private void OnEnable()
{
// 注册 Undo 回调SceneView 中拖拽房间后执行 Ctrl+Z窗口自动刷新
Undo.undoRedoPerformed += OnUndoRedo;
}
private void OnDisable()
{
Undo.undoRedoPerformed -= OnUndoRedo;
}
/// <summary>Undo/Redo 发生后清除验证缓存并触发重绘,确保布局视图与数据同步。</summary>
private void OnUndoRedo()
{
_validationErrors = null;
_errorRoomIds = null;
Repaint();
}
private void OnGUI()
{
DrawToolbar();

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5c889260fa9407545a7db0a014e1e176
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -39,7 +39,10 @@ namespace BaseGames.Editor.Map
EditorGUILayout.HelpBox(
"在 Scene View 中可直接拖拽房间角点调整 GridPosition / GridSize。\n" +
"拖动自动吸附到 1 格精度,支持 Undo。",
"拖动自动吸附到 1 格精度,支持 Undo。\n\n" +
"⚠ 坐标系说明Scene View 中 1 格 = 1 世界单位(编辑器可视化坐标)。\n" +
"运行时玩家追踪使用 worldUnitsPerCell默认 18 世界单位/格)。\n" +
"两者仅为独立坐标系互不影响——格子布局数据GridPosition/GridSize是统一的格子单位无需换算。",
MessageType.Info);
if (GUILayout.Button("居中 Scene View 到此房间", GUILayout.Height(28)))

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 96f79378a72e0884eb67abdec7cedd2a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2f10ab54d55ebf14a8c93cca7164230c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f8bb8a918cea77d4097634a071a13b4b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4868a5b10a549ea43826ff162ecc6b5e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e0c5d13ad97e89a4b8f49b35620789c5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -94,7 +94,7 @@ namespace BaseGames.World.Map
private void OnValidate() => _index = null; // 编辑器中修改 AllRooms 后强制重建索引
// ── 配置验证 ──────────────────────────────────────────────────────────
#if UNITY_EDITOR
/// <summary>
/// 检查数据库中的常见配置错误RoomId 重复、格子重叠、出口悬空)。
/// 编辑器侧调用;运行时不应调用(有 O(N²) 开销)。
@@ -152,5 +152,6 @@ namespace BaseGames.World.Map
return errors;
}
#endif
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9a93d2e2dddd51740807bd2ceebb68c9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -42,6 +42,11 @@ namespace BaseGames.World.Map
// 复用 List 避免 RefreshView 每次分配临时 ListGC 友好)
private readonly List<string> _toRemove = new List<string>(8);
// 空间索引:格子坐标 → 房间 ID将 RefreshView step② 的 O(N) 遍历降至 O(viewRadius²)
private Dictionary<Vector2Int, string> _spatialIndex;
// 复用 HashSet 避免 RefreshView 每次分配GC 友好)
private readonly HashSet<string> _roomsInViewBuffer = new HashSet<string>(32);
private Vector2Int _currentCenter;
private string _lastDotRoomId;
private Vector2 _lastDotNormPos;
@@ -53,6 +58,8 @@ namespace BaseGames.World.Map
_mapSvc = ServiceLocator.GetOrDefault<IMapService>();
_playerProvider = ServiceLocator.GetOrDefault<IPlayerPositionProvider>();
BuildSpatialIndex(_mapSvc?.Database);
if (_playerProvider != null)
_playerProvider.OnRoomChanged += OnRoomChanged;
@@ -72,6 +79,7 @@ namespace BaseGames.World.Map
_lastDotRoomId = null;
_mapSvc = null;
_playerProvider = null;
_spatialIndex = null;
}
private void ClearAllCells()
@@ -81,6 +89,24 @@ namespace BaseGames.World.Map
_cells.Clear();
}
/// <summary>
/// 构建格子坐标 → 房间 ID 的哈希映射。
/// 将 RefreshView step② 从 O(allRooms) 全量遍历降至 O(viewRadius²) 范围格点查询。
/// 数据库变更时(如热更)应再次调用。
/// </summary>
private void BuildSpatialIndex(MapDatabaseSO db)
{
_spatialIndex = new Dictionary<Vector2Int, string>();
if (db?.AllRooms == null) return;
foreach (var room in db.AllRooms)
{
if (room == null) continue;
for (int x = 0; x < room.GridSize.x; x++)
for (int y = 0; y < room.GridSize.y; y++)
_spatialIndex[new Vector2Int(room.GridPosition.x + x, room.GridPosition.y + y)] = room.RoomId;
}
}
private void LateUpdate()
{
UpdatePlayerDot();
@@ -133,16 +159,30 @@ namespace BaseGames.World.Map
}
foreach (var id in _toRemove) _cells.Remove(id);
// ② 实例化新进入范围的格子
foreach (var room in db.AllRooms)
// ② 用空间索引替代 O(N) 全量遍历,在可视范围格点上查询所属房间
// 复杂度O(viewRadius²) 替代 O(allRooms),大地图下效果显著
_roomsInViewBuffer.Clear();
if (_spatialIndex != null)
{
if (room == null || _cells.ContainsKey(room.RoomId)) continue;
if (!RoomInView(room, minX, maxX, minY, maxY)) continue;
for (int x = minX; x <= maxX; x++)
for (int y = minY; y <= maxY; y++)
{
if (_spatialIndex.TryGetValue(new Vector2Int(x, y), out var rId))
_roomsInViewBuffer.Add(rId);
}
}
foreach (var roomId in _roomsInViewBuffer)
{
if (_cells.ContainsKey(roomId)) continue;
var room = db.GetRoom(roomId);
if (room == null) continue;
var cell = Instantiate(_cellPrefab, _cellContainer);
cell.Setup(room, _mapSvc.GetVisibility(room.RoomId), null);
cell.SetColors(_colorExplored, _colorMapped, _colorUnknown);
_cells[room.RoomId] = cell;
PlaceCell(cell, room); // 立即设置正确的中心相对坐标,避免 Setup 默认偏移被 step③ 覆盖
_cells[roomId] = cell;
}
// ③ 重定位所有格子(中心发生变化时)

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 02879db752b9f0b4bb1b0c9834ad4d84
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: