From e2bc3249057e0b155116f4e56d99fa97f2542887 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Mon, 25 May 2026 14:44:31 +0800 Subject: [PATCH] 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 --- .../Editor/World/Map/MapDatabaseEditor.cs | 22 +- .../World/Map/MapDatabaseEditor.cs.meta | 11 + .../Editor/World/Map/MapLayoutEditorWindow.cs | 19 + .../World/Map/MapLayoutEditorWindow.cs.meta | 11 + .../Editor/World/Map/MapRoomDataEditor.cs | 5 +- .../Scripts/World/Map/IPinService.cs.meta | 11 + .../World/Map/IPlayerPositionProvider.cs.meta | 11 + .../World/Map/MapGridConstants.cs.meta | 11 + .../Scripts/World/Map/MapInputHandler.cs.meta | 11 + .../Scripts/World/Map/MapRoomCellUI.cs.meta | 11 + .../_Game/Scripts/World/Map/MapRoomDataSO.cs | 3 +- .../World/Map/MapServiceExtensions.cs.meta | 11 + Assets/_Game/Scripts/World/Map/MinimapHUD.cs | 50 +- .../Scripts/World/Map/MinimapHUD.cs.meta | 11 + .../Minimap_Review_Round6_Independent.md | 499 ++++++++++++++++++ .../Minimap_Review_Round7_Independent.md | 368 +++++++++++++ zeling_v2.sln | 12 +- 17 files changed, 1060 insertions(+), 17 deletions(-) create mode 100644 Assets/_Game/Scripts/Editor/World/Map/MapDatabaseEditor.cs.meta create mode 100644 Assets/_Game/Scripts/Editor/World/Map/MapLayoutEditorWindow.cs.meta create mode 100644 Assets/_Game/Scripts/World/Map/IPinService.cs.meta create mode 100644 Assets/_Game/Scripts/World/Map/IPlayerPositionProvider.cs.meta create mode 100644 Assets/_Game/Scripts/World/Map/MapGridConstants.cs.meta create mode 100644 Assets/_Game/Scripts/World/Map/MapInputHandler.cs.meta create mode 100644 Assets/_Game/Scripts/World/Map/MapRoomCellUI.cs.meta create mode 100644 Assets/_Game/Scripts/World/Map/MapServiceExtensions.cs.meta create mode 100644 Assets/_Game/Scripts/World/Map/MinimapHUD.cs.meta create mode 100644 Docs/Review/Minimap_Review_Round6_Independent.md create mode 100644 Docs/Review/Minimap_Review_Round7_Independent.md diff --git a/Assets/_Game/Scripts/Editor/World/Map/MapDatabaseEditor.cs b/Assets/_Game/Scripts/Editor/World/Map/MapDatabaseEditor.cs index d203a35..837a73a 100644 --- a/Assets/_Game/Scripts/Editor/World/Map/MapDatabaseEditor.cs +++ b/Assets/_Game/Scripts/Editor/World/Map/MapDatabaseEditor.cs @@ -29,10 +29,26 @@ namespace BaseGames.Editor.Map /// private readonly HashSet _cachedErrorRoomIds = new(); + /// 错误行文本颜色样式,惰性初始化后复用,避免 OnInspectorGUI 每帧分配。 + 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; // 编辑器皮肤切换时(亮/暗模式)需重建 + } + + /// 错误行 GUIStyle 惰性初始化,基于当前编辑器皮肤构建,避免 OnInspectorGUI 每帧分配。 + 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( diff --git a/Assets/_Game/Scripts/Editor/World/Map/MapDatabaseEditor.cs.meta b/Assets/_Game/Scripts/Editor/World/Map/MapDatabaseEditor.cs.meta new file mode 100644 index 0000000..69b205d --- /dev/null +++ b/Assets/_Game/Scripts/Editor/World/Map/MapDatabaseEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e42e54d73570d0245b6bb4e722c3b0f8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Game/Scripts/Editor/World/Map/MapLayoutEditorWindow.cs b/Assets/_Game/Scripts/Editor/World/Map/MapLayoutEditorWindow.cs index ebc4de8..cc24163 100644 --- a/Assets/_Game/Scripts/Editor/World/Map/MapLayoutEditorWindow.cs +++ b/Assets/_Game/Scripts/Editor/World/Map/MapLayoutEditorWindow.cs @@ -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; + } + + /// Undo/Redo 发生后清除验证缓存并触发重绘,确保布局视图与数据同步。 + private void OnUndoRedo() + { + _validationErrors = null; + _errorRoomIds = null; + Repaint(); + } + private void OnGUI() { DrawToolbar(); diff --git a/Assets/_Game/Scripts/Editor/World/Map/MapLayoutEditorWindow.cs.meta b/Assets/_Game/Scripts/Editor/World/Map/MapLayoutEditorWindow.cs.meta new file mode 100644 index 0000000..13d9611 --- /dev/null +++ b/Assets/_Game/Scripts/Editor/World/Map/MapLayoutEditorWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5c889260fa9407545a7db0a014e1e176 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Game/Scripts/Editor/World/Map/MapRoomDataEditor.cs b/Assets/_Game/Scripts/Editor/World/Map/MapRoomDataEditor.cs index a6c2c9b..a1325d3 100644 --- a/Assets/_Game/Scripts/Editor/World/Map/MapRoomDataEditor.cs +++ b/Assets/_Game/Scripts/Editor/World/Map/MapRoomDataEditor.cs @@ -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))) diff --git a/Assets/_Game/Scripts/World/Map/IPinService.cs.meta b/Assets/_Game/Scripts/World/Map/IPinService.cs.meta new file mode 100644 index 0000000..fc07b5f --- /dev/null +++ b/Assets/_Game/Scripts/World/Map/IPinService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 96f79378a72e0884eb67abdec7cedd2a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Game/Scripts/World/Map/IPlayerPositionProvider.cs.meta b/Assets/_Game/Scripts/World/Map/IPlayerPositionProvider.cs.meta new file mode 100644 index 0000000..8c633c9 --- /dev/null +++ b/Assets/_Game/Scripts/World/Map/IPlayerPositionProvider.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2f10ab54d55ebf14a8c93cca7164230c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Game/Scripts/World/Map/MapGridConstants.cs.meta b/Assets/_Game/Scripts/World/Map/MapGridConstants.cs.meta new file mode 100644 index 0000000..d5d2456 --- /dev/null +++ b/Assets/_Game/Scripts/World/Map/MapGridConstants.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f8bb8a918cea77d4097634a071a13b4b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Game/Scripts/World/Map/MapInputHandler.cs.meta b/Assets/_Game/Scripts/World/Map/MapInputHandler.cs.meta new file mode 100644 index 0000000..432451f --- /dev/null +++ b/Assets/_Game/Scripts/World/Map/MapInputHandler.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4868a5b10a549ea43826ff162ecc6b5e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Game/Scripts/World/Map/MapRoomCellUI.cs.meta b/Assets/_Game/Scripts/World/Map/MapRoomCellUI.cs.meta new file mode 100644 index 0000000..fb12184 --- /dev/null +++ b/Assets/_Game/Scripts/World/Map/MapRoomCellUI.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e0c5d13ad97e89a4b8f49b35620789c5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Game/Scripts/World/Map/MapRoomDataSO.cs b/Assets/_Game/Scripts/World/Map/MapRoomDataSO.cs index e9273e1..506695c 100644 --- a/Assets/_Game/Scripts/World/Map/MapRoomDataSO.cs +++ b/Assets/_Game/Scripts/World/Map/MapRoomDataSO.cs @@ -94,7 +94,7 @@ namespace BaseGames.World.Map private void OnValidate() => _index = null; // 编辑器中修改 AllRooms 后强制重建索引 // ── 配置验证 ────────────────────────────────────────────────────────── - +#if UNITY_EDITOR /// /// 检查数据库中的常见配置错误(RoomId 重复、格子重叠、出口悬空)。 /// 编辑器侧调用;运行时不应调用(有 O(N²) 开销)。 @@ -152,5 +152,6 @@ namespace BaseGames.World.Map return errors; } +#endif } } diff --git a/Assets/_Game/Scripts/World/Map/MapServiceExtensions.cs.meta b/Assets/_Game/Scripts/World/Map/MapServiceExtensions.cs.meta new file mode 100644 index 0000000..edf5f43 --- /dev/null +++ b/Assets/_Game/Scripts/World/Map/MapServiceExtensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9a93d2e2dddd51740807bd2ceebb68c9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Game/Scripts/World/Map/MinimapHUD.cs b/Assets/_Game/Scripts/World/Map/MinimapHUD.cs index c313f21..5be66d2 100644 --- a/Assets/_Game/Scripts/World/Map/MinimapHUD.cs +++ b/Assets/_Game/Scripts/World/Map/MinimapHUD.cs @@ -42,6 +42,11 @@ namespace BaseGames.World.Map // 复用 List 避免 RefreshView 每次分配临时 List(GC 友好) private readonly List _toRemove = new List(8); + // 空间索引:格子坐标 → 房间 ID,将 RefreshView step② 的 O(N) 遍历降至 O(viewRadius²) + private Dictionary _spatialIndex; + // 复用 HashSet 避免 RefreshView 每次分配(GC 友好) + private readonly HashSet _roomsInViewBuffer = new HashSet(32); + private Vector2Int _currentCenter; private string _lastDotRoomId; private Vector2 _lastDotNormPos; @@ -53,6 +58,8 @@ namespace BaseGames.World.Map _mapSvc = ServiceLocator.GetOrDefault(); _playerProvider = ServiceLocator.GetOrDefault(); + 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(); } + /// + /// 构建格子坐标 → 房间 ID 的哈希映射。 + /// 将 RefreshView step② 从 O(allRooms) 全量遍历降至 O(viewRadius²) 范围格点查询。 + /// 数据库变更时(如热更)应再次调用。 + /// + private void BuildSpatialIndex(MapDatabaseSO db) + { + _spatialIndex = new Dictionary(); + 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; } // ③ 重定位所有格子(中心发生变化时) diff --git a/Assets/_Game/Scripts/World/Map/MinimapHUD.cs.meta b/Assets/_Game/Scripts/World/Map/MinimapHUD.cs.meta new file mode 100644 index 0000000..a5b15e4 --- /dev/null +++ b/Assets/_Game/Scripts/World/Map/MinimapHUD.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 02879db752b9f0b4bb1b0c9834ad4d84 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Docs/Review/Minimap_Review_Round6_Independent.md b/Docs/Review/Minimap_Review_Round6_Independent.md new file mode 100644 index 0000000..303ca72 --- /dev/null +++ b/Docs/Review/Minimap_Review_Round6_Independent.md @@ -0,0 +1,499 @@ +# 小地图系统 Round 6 独立评估报告 + +> **评估日期**:Round 6(基于当前代码状态) +> **评估方法**:独立从零阅读所有 14 个地图模块文件,不参考 Round 5 自评结论 +> **对标标准**:成熟 2D Metroidvania 游戏小地图,专注编辑器扩展质量、架构解耦、高性能、策划友好度 +> **注意**:本轮评估目标是发现 Round 5 自评可能存在的通胀,以及 Round 4 修复后遗留的真实问题 + +--- + +## 一、所有评估文件清单 + +| 文件 | 类型 | +|---|---| +| `IMapService.cs` | 运行时接口 | +| `IPlayerPositionProvider.cs` | 运行时接口(新) | +| `IPinService.cs` | 运行时接口(新) | +| `MapGridConstants.cs` | 运行时常量(新) | +| `MapServiceExtensions.cs` | 运行时扩展方法 | +| `MapRoomDataSO.cs` + `MapDatabaseSO` | 数据层 SO | +| `MapManager.cs` | 运行时管理器 | +| `MapPlayerTracker.cs` | 运行时玩家追踪 | +| `MapPin.cs`(MapPinManager) | 运行时标记管理 | +| `MapRoomCellUI.cs` | 运行时 UI 格子 | +| `MapPanel.cs` | 运行时全屏地图 | +| `MinimapHUD.cs` | 运行时小地图 HUD | +| `MapInputHandler.cs` | 运行时输入处理 | +| `RegionNameDisplay.cs` | 运行时区域名显示 | +| `MapRoomDataEditor.cs` | 编辑器扩展 | +| `MapDatabaseEditor.cs` | 编辑器扩展 | +| `MapLayoutEditorWindow.cs` | 编辑器扩展 | + +--- + +## 二、逐维度独立评分 + +### 维度 1:架构解耦(Architecture Decoupling) + +#### ✅ 优点 + +**接口三层完整:** +``` +IMapService ← ServiceLocator ← MapManager (ISaveable) +IPlayerPositionProvider ← ServiceLocator ← MapPlayerTracker +IPinService ← ServiceLocator ← MapPinManager (ISaveable) +``` + +- `MapPanel` 和 `MinimapHUD` **零具体类 SerializeField 引用**,仅通过接口消费 ✓ +- ServiceLocator 注册/注销生命周期管理正确: + - MapManager: `Awake` 注册,单例保护,`OnDestroy` 注销 ✓ + - MapPlayerTracker: `Awake` 注册(带 GetOrDefault 保护),`OnDestroy` 注销 ✓ + - MapPinManager: `OnEnable`/`OnDisable` 注册/注销,与 ISaveableRegistry 同步 ✓ +- `MapServiceExtensions.GetVisibility()` 将三级状态推导逻辑集中在一处,消费方零重复 ✓ +- `OnDisable` 时清空所有接口引用,避免悬挂引用 ✓ + +#### ⚠️ 问题 + +1. **`MapDatabaseSO` 双重职责**:既是数据容器(`AllRooms[]`)又是服务类(`GetRoom()`、`ValidateAll()`)。`ValidateAll` 是 O(N²) 操作且只应在编辑器调用,但它存在于运行时 SO 中——缺少 `#if UNITY_EDITOR` 保护或独立的验证服务类。 +2. **`MapRoomDataSO` 与 `MapDatabaseSO` 同文件**:Unity 约定每个 SO 类独立一个文件,同文件降低可发现性。 +3. **`IMapService.Database` 属性暴露了具体类型 `MapDatabaseSO`**:接口直接暴露具体 SO 类,消费方(MinimapHUD、MapPanel)均直接访问 `Database.AllRooms`,若未来替换数据源需修改接口。 + +**评分:9/10**(接口体系完整,主要扣分点:Database 属性暴露具体类,ValidateAll 无编辑器保护) + +--- + +### 维度 2:性能(Performance) + +#### ✅ 已解决的热路径 + +| 问题 | 解决方案 | +|---|---| +| `NormalizedPositionInRoom` 离散跳动 | 世界坐标精确插值 | +| `Canvas.ForceUpdateCanvases()` 全树刷新 | `LayoutRebuilder.ForceRebuildLayoutImmediate(content)` 局部 | +| Pin 重绘无脏检查 | `_lastPinVersion` dirty check | +| `GetPinSprite` O(N) 循环 | `Dictionary` O(1) 查找 | +| MapPlayerTracker 每帧 GetRoom 查找 | `_currentRoom` 缓存 | +| LateUpdate 每帧写 RectTransform | dirty check(位置 + 房间 ID 双重检查) | +| MinimapHUD `_toRemove` 每次分配 | 字段复用 | +| `GUIStyle` 每帧每格创建 | `EnsureLabelStyles()` 带缩放变化检测 | + +#### ⚠️ 遗留性能问题 + +**P1(真实瓶颈):MinimapHUD.RefreshView O(N) 遍历** + +```csharp +// MinimapHUD.cs 第137行 +foreach (var room in db.AllRooms) // O(N) 遍历全部房间 +{ + if (room == null || _cells.ContainsKey(room.RoomId)) continue; + if (!RoomInView(room, minX, maxX, minY, maxY)) continue; + ... +} +``` + +每次房间切换(`OnRoomChanged`)遍历 `db.AllRooms` 全量扫描。房间数 ≤ 50 时影响不大,100+ 房间时每次切换房间触发 O(N) 遍历。正确做法应为空间分区(`Dictionary`)。 + +**P2(编辑器):MapDatabaseEditor 错误行样式每帧分配** + +```csharp +// MapDatabaseEditor.cs 第125-127行 +var rowStyle = hasError + ? new GUIStyle(EditorStyles.label) { normal = { textColor = new Color(1f, 0.35f, 0.35f) } } + : EditorStyles.label; +``` + +每次 `OnInspectorGUI` 重绘(高频)为每个错误房间分配新 `GUIStyle`,应缓存为字段(类比 `MapLayoutEditorWindow` 的 `_roomLabelStyle` 做法)。 + +**P3(低频):`GetRoomsByRegion` 每次 LINQ + `ToArray` 分配** + +```csharp +return _database.AllRooms.Where(r => r != null && r.RegionId == regionId).ToArray(); +``` + +调用频率低,但每次调用均堆分配新数组,可改为返回 `IReadOnlyList` + 缓存或仅在热路径上注意。 + +**评分:8/10**(热路径整体优化良好,MinimapHUD O(N) 遍历是真实未修复问题,编辑器 GUIStyle 分配) + +--- + +### 维度 3:编辑器扩展质量(Editor Extension Quality) + +#### ✅ 优点 + +**MapRoomDataEditor(SceneView 拖拽)** +- 双角控制点 BL/TR,精度 1 格 ✓ +- `Undo.RecordObject` 支持撤销 ✓ +- 防反转保护(`Mathf.Min/Max`) ✓ +- 一键居中 SceneView ✓ +- 房间 ID 标签 + 颜色可读 ✓ + +**MapDatabaseEditor(Inspector 增强)** +- 统计摘要(房间数、出口总数) ✓ +- 一键验证 + HelpBox 错误列表 ✓ +- `_cachedErrorRoomIds` 仅按钮点击时重建,避免高频 GC ✓ +- 引号精确匹配(`"'{r.RoomId}'"` 防前缀误判) ✓ +- 一键打开布局编辑器 + `SetDatabase()` 公共 API ✓ +- 可折叠房间列表 + Ping ✓ + +**MapLayoutEditorWindow(独立窗口)** +- 全局地图实时预览 ✓ +- 滚轮缩放 + 中键/Alt拖拽平移 ✓ +- 区域自动配色(8 色方案) ✓ +- 验证错误红色高亮 ✓ +- 点击选中房间 → Selection + Ping ✓ +- 出口连线(`_zoom >= 12f` 时显示) ✓ +- `GUIStyle` 缓存(`EnsureLabelStyles`,zoom 变化时重建) ✓ + +#### ⚠️ 问题 + +1. **MapDatabaseEditor 错误行样式未缓存**(同 P2,编辑器性能问题,见上文) +2. **无 `Undo.undoRedoPerformed` 回调**:在 MapRoomDataEditor 中通过 SceneView 拖拽修改后,执行 Ctrl+Z 撤销,MapLayoutEditorWindow 不会自动刷新视图,需手动点击 Repaint。 +3. **无批量房间创建向导**:策划需逐个手动创建 `MapRoomDataSO.asset`,缺少"从场景列表批量生成"工具。 +4. **MapRoomDataEditor.CELL_SIZE 硬编码为 1f**:SceneView 控制点坐标基于 1 格 = 1 世界单位。如果关卡实际比例不同(`MapPlayerTracker._worldUnitsPerCell = 18f`),SceneView 中的可视化与实际地图 UI 比例不一致,容易混淆策划。 + +**评分:8.5/10**(三工具功能完整,主要缺口:Undo 回调刷新、批量创建向导、SceneView 与 UI 比例不一致的隐患) + +--- + +### 维度 4:数据设计(Data Design) + +#### ✅ 优点 + +- `MapRoomDataSO` 字段语义完整:基础信息、格子坐标、轮廓纹理、出口数据、特殊标记、流式加载内存预估 ✓ +- `RoomExitData.PreferredTransitionType`:区分 `Seamless`/`AtmosphericFade`,过渡类型枚举化 ✓ +- `MapDatabaseSO.GetRoom()` 懒加载索引 + `OnValidate`/`OnDisable` 缓存清理 ✓ +- 四类验证覆盖:null、RoomId 重复、格子重叠、出口悬空 ✓ +- `MapRoomDataSO.OnValidate` 保护 GridSize 最小值 ✓ +- `PinSpriteEntry` 移入 `MapPin.cs`,数据与逻辑同文件 ✓ + +#### ⚠️ 问题 + +1. **无 RegionSO 数据资产**:`RegionId` 是 `string`,区域的颜色主题、显示名、排序等属性无处集中配置,目前散落在: + - `RegionNameDisplay._regionNames`(运行时组件) + - `MapLayoutEditorWindow.Palette`(编辑器,按发现顺序自动分配,不可控) +2. **`ValidateAll` 仅单向出口验证**:验证 A→B 存在,但不验证 B→A 是否存在(非双向可能是有意设计,但应有注释说明)。 +3. **RoomId 与场景名一致约定无自动验证**:注释中要求 `RoomId` 与场景名一致(`"Room_Forest_01"`),但验证逻辑中不检查此约束。 +4. **`ExitGridPos` 未验证是否在房间边界上**:出口坐标可以配置在房间内部或完全不相关的位置,验证工具不检测此异常。 + +**评分:8/10**(数据设计扎实,主要缺口:缺少 RegionSO、出口验证不够严格) + +--- + +### 维度 5:功能完整性(Feature Completeness) + +#### 已实现功能矩阵 + +| 功能 | 状态 | +|---|---| +| 三级可见性(Unknown/Explored/Mapped) | ✅ 完整 | +| 存档持久化(探索状态 + 标记) | ✅ 完整 | +| 全屏地图面板 + ScrollRect | ✅ 完整 | +| 角落小地图 HUD | ✅ 完整 | +| 玩家位置图标(平滑跟随) | ✅ 完整 | +| 当前房间高亮描边 | ✅ 完整 | +| 自定义标记 Pin | ✅ 完整 | +| 出口连接线显示 | ✅ 完整 | +| 鼠标滚轮 + 键盘平移缩放 | ✅ 完整 | +| 区域变化广播 + 区域名渐显动画 | ✅ 完整 | +| 房间 Tooltip(DisplayName) | ✅ 完整 | +| MapFragment 购买揭示(SetMapped) | ✅ 接口完整 | +| 探索进度 API | ✅ 接口完整 | +| 非矩形房间轮廓纹理 | ✅ 字段完整(UI 已接入) | + +#### 缺失/待完善功能 + +1. **探索进度无 HUD 显示**:`IMapService.GetExplorationProgress()` + `ExploredRoomCount` 已实现,但 UI 层(MapPanel、MinimapHUD)均未显示进度条或百分比文本。 +2. **移动端 / 手柄支持**:`MapInputHandler` 使用旧版 `Input.GetAxisRaw`,无 Pinch 缩放手势,移动端不可用。 +3. **小地图运行时缩放调节**:MinimapHUD 的 `_cellPixels` 只可在 Inspector 配置,玩家运行时无法调整小地图大小(无障碍缺口)。 +4. **标记类型不足**:Pin 只有基础 `PinType` 枚举(定义在 SaveData.cs),缺少游戏中常见的「未解谜」「隐藏通道」「收藏品」等语义标记类型的默认配置。 +5. **全屏地图与小地图无联动隐藏**:打开全屏地图时 MinimapHUD 不自动隐藏,视觉上有重叠(需外部 UIManager 处理,系统内无协调机制)。 + +**评分:8/10**(核心功能覆盖全面,缺失点为进度显示、移动端支持、小地图缩放) + +--- + +### 维度 6:代码质量(Code Quality) + +#### ✅ 优点 + +- 命名规范一致:`_camelCase` 私有字段,`PascalCase` 公有成员 ✓ +- XML 文档注释覆盖所有公共 API ✓ +- `//──` 区域注释辅助导航 ✓ +- no-game-references 规则完全遵守 ✓ +- `MapServiceExtensions` 消除消费方重复的三级状态判断逻辑 ✓ +- `MapGridConstants.FullMapCellPixels` 消除 3 处散落的 `32f` 硬编码 ✓ +- `CompositeDisposable _subs` 统一管理事件订阅生命周期 ✓ + +#### ⚠️ 问题 + +1. **`MapPin.cs` 文件名与类名不一致**:文件包含 `MapPinManager` 类,文件头虽有注释说明,但工具搜索"MapPin.cs"不会找到 `MapPinManager`。这是历史遗留问题,需在安全时机重命名。 +2. **`MapDatabaseEditor` 错误行样式每帧分配**(同 P2): + ```csharp + var rowStyle = hasError + ? new GUIStyle(EditorStyles.label) { ... } // ← 每次 OnInspectorGUI 分配 + : EditorStyles.label; + ``` +3. **`MapManager.GetRoomsByRegion` 每次 LINQ + ToArray**:返回 `MapRoomDataSO[]`,调用方每次得到新数组。建议改为按需缓存或返回 `IReadOnlyList`。 +4. **`MapPanel.DrawExits` 中的硬编码尺寸**: + ```csharp + conn.rectTransform.sizeDelta = vertical ? new Vector2(16f, 8f) : new Vector2(8f, 16f); + ``` + 出口连接线尺寸硬编码为 `16f/8f`,未使用 `MapGridConstants` 派生,与全屏地图的格子尺寸(32f)比例关系不透明。 +5. **`RegionNameDisplay.ResolveDisplayName` O(N) 线性查找**:区域数量通常 < 20,影响微小,但与 GetPinSprite 字典化处理的一致性不足。 + +**评分:8.5/10**(整体规范高,主要扣分点:文件名不一致、MapDatabaseEditor GUIStyle 分配、DrawExits 硬编码) + +--- + +### 维度 7:可扩展性(Extensibility) + +#### ✅ 优点 + +- 接口层允许替换实现(测试时可 Mock `IMapService`) ✓ +- `PinType` 枚举可在 SaveData.cs 扩展 + `_pinSprites` Inspector 配置 ✓ +- 颜色完全可配置(SerializeField on MapPanel, MinimapHUD) ✓ +- 事件渠道(EventChannelSO)允许外部任意系统订阅地图事件 ✓ +- `MapRoomDataSO` SO 驱动,增加房间只需新建资产 ✓ +- `MapServiceExtensions` 扩展方法机制允许在不修改接口的情况下添加新查询 ✓ + +#### ⚠️ 问题 + +1. **MapPanel 无覆盖层(Overlay)扩展点**:添加「区域边界覆盖」「探索热度图」等新层需要直接修改 `MapPanel.cs`,无法通过组件/插件扩展。 +2. **`MapRoomDataSO` 无自定义扩展字段**:新增房间属性(如「危险等级」「推荐等级要求」)需修改 SO 类,无法通过 ScriptableObject 继承或 payload 扩展。 +3. **`IMapService.Database` 返回具体类**:(同架构问题)限制了未来数据源替换(如从服务器加载、动态生成)的可能性。 + +**评分:8/10**(接口 + 事件体系支持良好扩展,主要缺口:MapPanel 无 Overlay 系统,MapRoomDataSO 无扩展字段) + +--- + +### 维度 8:策划友好度(Designer/Planner Friendliness) + +#### ✅ 优点 + +- `MapRoomDataSO` 字段有中文 Header + 详细 Tooltip(`EstimatedMemoryKB`、`PreferredTransitionType` 均有使用说明)✓ +- `MapDatabaseEditor` 提供统计摘要、一键验证、可折叠房间列表 ✓ +- `MapRoomDataEditor` 支持 SceneView 直接拖拽调整房间位置和大小 ✓ +- `MapLayoutEditorWindow` 直观预览全局地图 + 区域配色区分 ✓ +- 验证错误以 HelpBox/红色高亮明确指出有问题的房间 ✓ + +#### ⚠️ 问题 + +1. **无 MapDesignSpec 文档**:缺少指导策划配置地图的设计规范文档(`Docs/Standards/MapDesignSpec.md`),策划需从代码注释推断配置规则。 +2. **无批量房间创建向导**:每个房间需单独创建 SO 并手动填写 RoomId,100 个房间需重复 100 次相同操作。 +3. **`MapLayoutEditorWindow` 无探索状态预览**:窗口只显示房间布局,无法预览各可见性状态(Unknown/Explored/Mapped)下的视觉效果。 +4. **`RegionId` 无枚举约束**:策划可以在不同 SO 中输入 `"Forest"` 和 `"forest"` 导致区域不匹配,缺少下拉选择或自动补全。 + +**评分:8/10**(工具覆盖主要流程,策划可独立使用,主要缺口:缺乏文档、批量工具、RegionId 输入约束) + +--- + +## 三、与 Round 5 自评对比 + +| 维度 | Round 5 自评 | Round 6 独立评分 | 差值 | 主要差异 | +|---|---|---|---|---| +| 架构解耦 | 9.5/10 | 9/10 | -0.5 | Database 属性暴露具体类型 | +| 性能 | 9/10 | 8/10 | **-1** | MinimapHUD O(N)遍历未修复 | +| 编辑器扩展 | 9/10 | 8.5/10 | -0.5 | 无 Undo 刷新回调,GUIStyle 分配 | +| 数据设计 | 8.5/10 | 8/10 | -0.5 | 缺 RegionSO,出口单向验证 | +| 功能完整性 | 8/10 | 8/10 | 0 | 一致 | +| 代码质量 | 8.5/10 | 8.5/10 | 0 | 一致 | +| 可扩展性 | 8/10 | 8/10 | 0 | 一致 | +| 策划友好度 | 8/10 | 8/10 | 0 | 一致 | +| **加权总分** | **90/100** | **85/100** | **-5** | | + +> Round 5 总分通胀约 **+5 分**,来源:性能维度对 MinimapHUD 问题评估过松(标注为已知但仍给 9 分),以及架构维度未扣 Database 暴露具体类分。 + +--- + +## 四、Round 6 发现的新问题(Round 5 未标注) + +### 🔴 N1:MapDatabaseEditor 错误行 GUIStyle 每帧分配 + +**位置**:`MapDatabaseEditor.cs` 第 125-127 行 + +```csharp +var rowStyle = hasError + ? new GUIStyle(EditorStyles.label) { normal = { textColor = new Color(1f, 0.35f, 0.35f) } } + : EditorStyles.label; +``` + +**问题**:`OnInspectorGUI` 每次重绘(约 10-30 fps)为每个错误房间分配新 `GUIStyle`。`MapLayoutEditorWindow` 已通过 `EnsureLabelStyles()` 正确缓存,但 `MapDatabaseEditor` 遗漏了相同处理。 + +**修复方案**: +```csharp +// 在类中添加缓存字段 +private GUIStyle _errorRowStyle; + +// 在 OnEnable 或首次使用时初始化 +private GUIStyle GetErrorRowStyle() +{ + if (_errorRowStyle == null) + _errorRowStyle = new GUIStyle(EditorStyles.label) { normal = { textColor = new Color(1f, 0.35f, 0.35f) } }; + return _errorRowStyle; +} + +// 使用时 +var rowStyle = hasError ? GetErrorRowStyle() : EditorStyles.label; +``` + +--- + +### 🔴 N2:MapRoomDataEditor.CELL_SIZE 与 MapPlayerTracker._worldUnitsPerCell 比例不一致隐患 + +**位置**:`MapRoomDataEditor.cs` 第 16 行 vs `MapPlayerTracker.cs` 第 25 行 + +```csharp +// MapRoomDataEditor.cs +private const float CELL_SIZE = 1f; // SceneView 中每格 = 1 世界单位 + +// MapPlayerTracker.cs +[SerializeField] private float _worldUnitsPerCell = 18f; // 运行时每格 = 18 世界单位 +``` + +**问题**: +- SceneView 中拖拽房间角点的坐标系使用 1 格 = 1 世界单位 +- 运行时玩家追踪的坐标系使用 1 格 = 18 世界单位 +- 两套坐标系并存,策划在 SceneView 中看到的房间布局与实际游戏世界坐标无直接对应关系 +- 若策划以为 SceneView 中调整的就是游戏内的实际空间,会在关卡设计时产生误解 + +**建议**:在 `MapRoomDataEditor` 的 SceneView HelpBox 中明确说明此坐标换算,或将 `CELL_SIZE` 从 `MapGridConstants` 派生(需将 CELL_SIZE 添加到常量类)。 + +--- + +### 🟡 N3:MinimapHUD.RefreshView 中 `db.AllRooms` 遍历顺序不稳定问题 + +当 `RefreshView` 先回收旧格子,再添加新格子时: + +```csharp +// 步骤③:重定位全部格子(含新加入的) +foreach (var (id, cell) in _cells) +{ + if (cell == null) continue; + var r = db.GetRoom(id); // ← 每次 GetRoom 触发索引查找,O(1) 但有 dict 访问开销 + if (r != null) PlaceCell(cell, r); +} +``` + +在步骤②已经通过 `cell.Setup()` 设置了格子位置(基于 `MapGridConstants.FullMapCellPixels`),步骤③立即通过 `PlaceCell` 用 `_cellPixels` 覆盖。这意味着 `Setup()` 的位置设置对 MinimapHUD 完全无效,每次都被 `PlaceCell` 覆盖,产生不必要的 UI 操作。 + +**建议**:MinimapHUD 在 Instantiate 后直接调用 `PlaceCell` 而不通过 `Setup()` 传入默认 pixelsPerCell。 + +--- + +### 🟡 N4:`MapInputHandler` 使用旧版 Input System + +```csharp +float h = Input.GetAxisRaw("Horizontal"); +float v = Input.GetAxisRaw("Vertical"); +``` + +若项目引入 Unity New Input System,此代码会失效(Input 轴未正确重定向时返回 0)。应通过 `IInputService` 或 `InputActions` 接入。 + +--- + +## 五、待修复优先级 + +### 优先级 P1(高:影响运行性能) +| ID | 问题 | 位置 | 影响 | +|---|---|---|---| +| P1-1 | MinimapHUD.RefreshView O(N) 遍历 | `MinimapHUD.cs:137` | 大型地图(100+房间)房间切换卡顿 | + +### 优先级 P2(中:影响编辑器体验) +| ID | 问题 | 位置 | 影响 | +|---|---|---|---| +| P2-1 | MapDatabaseEditor 错误行 GUIStyle 每帧分配 | `MapDatabaseEditor.cs:125-127` | 编辑器 GC 压力 | +| P2-2 | MapRoomDataEditor CELL_SIZE 与运行时比例不一致 | `MapRoomDataEditor.cs:16` | 策划混淆坐标系 | +| P2-3 | 无 Undo 刷新回调(MapLayoutEditorWindow) | `MapLayoutEditorWindow.cs` | SceneView Undo 后窗口不自动刷新 | +| P2-4 | ValidateAll 缺 `#if UNITY_EDITOR` 保护 | `MapRoomDataSO.cs:103` | 运行时构建包含 O(N²) 验证逻辑 | + +### 优先级 P3(低:架构改进) +| ID | 问题 | 位置 | 影响 | +|---|---|---|---| +| P3-1 | 缺少 RegionSO 数据资产 | 新文件 | 区域颜色/名称配置分散 | +| P3-2 | MapDesignSpec.md 文档缺失 | Docs/Standards | 策划无参考文档 | +| P3-3 | IMapService.Database 暴露具体类 | `IMapService.cs:12` | 限制未来数据源替换 | +| P3-4 | MapPanel 无 Overlay 扩展点 | `MapPanel.cs` | 新增覆盖层需修改源码 | +| P3-5 | GetRoomsByRegion LINQ 每次分配数组 | `MapManager.cs:114` | 低频但可优化 | + +--- + +## 六、Round 6 总评 + +### 最终得分:85/100 + +``` +架构解耦 ████████░░ 9/10 +性能 ████████░░ 8/10 ← MinimapHUD O(N) 真实问题 +编辑器扩展 ████████░░ 8.5/10 +数据设计 ████████░░ 8/10 +功能完整性 ████████░░ 8/10 +代码质量 ████████░░ 8.5/10 +可扩展性 ████████░░ 8/10 +策划友好度 ████████░░ 8/10 +───────────────────────────── +加权总分 85/100 +``` + +### 总结 + +经过 Round 4 的 14 项修复,系统已从 Round 3 膨胀的 89 分回落至真实的 ~79 分,再经修复提升至当前的 **85 分**。Round 5 自评的 90 分存在约 +5 分的评估偏高。 + +**系统优势**:接口解耦体系完整、热路径性能全面优化、编辑器三工具功能齐全、数据驱动设计规范。 + +**主要差距**: +1. MinimapHUD 在大型地图下有 O(N) 遍历真实瓶颈 +2. 缺少 RegionSO 资产导致区域配置分散 +3. 编辑器工具有小的 GC 问题和 Undo 刷新缺口 +4. 策划侧缺乏设计规范文档 + +达到 90+ 分需解决 P1-1(MinimapHUD 空间索引)和 P3-1(RegionSO)两个主要缺口。 + +--- + +--- + +## 七、修复实施结果追踪 + +> 本章记录根据 Round 6 评估结论对代码执行的实际修复,确保报告与代码状态同步。 + +### P1 修复(高优先级) + +| ID | 状态 | 修改文件 | 说明 | +|---|---|---|---| +| P1-1 MinimapHUD 空间索引 | ✅ 已修复 | `MinimapHUD.cs` | 新增 `_spatialIndex Dict` + `_roomsInViewBuffer HashSet`;`BuildSpatialIndex()` 在 `OnEnable` 构建;RefreshView step② 改为 O(viewRadius²) 空间索引查询替代 O(N) 全量遍历 | + +### P2 修复(中优先级 — 编辑器体验) + +| ID | 状态 | 修改文件 | 说明 | +|---|---|---|---| +| P2-1 MapDatabaseEditor GUIStyle 缓存 | ✅ 已修复 | `MapDatabaseEditor.cs` | 新增 `_errorRowStyle` 缓存字段 + `GetErrorRowStyle()` 惰性初始化;`OnEnable` 时置 null(编辑器皮肤切换时重建);替换 `OnInspectorGUI` 内每帧 `new GUIStyle()` 调用 | +| P2-2 MapRoomDataEditor CELL_SIZE 说明 | ✅ 已修复 | `MapRoomDataEditor.cs` | Inspector HelpBox 补充坐标系说明:CELL_SIZE=1f 仅为 SceneView 可视化单位,格子布局数据(GridPosition/GridSize)是统一的格子单位,与运行时 worldUnitsPerCell 无需换算 | +| P2-3 MapLayoutEditorWindow Undo 刷新 | ✅ 已修复 | `MapLayoutEditorWindow.cs` | 新增 `OnEnable`/`OnDisable` 注册/注销 `Undo.undoRedoPerformed`;`OnUndoRedo()` 清除验证缓存并调用 `Repaint()` | +| P2-4 ValidateAll 编辑器保护 | ✅ 已修复 | `MapRoomDataSO.cs` | `ValidateAll()` 方法用 `#if UNITY_EDITOR` 包裹,O(N²) 验证逻辑不再进入运行时构建;两个调用方编辑器文件本身已在 `#if UNITY_EDITOR` 中,编译无变化 | + +### N3 修复(MinimapHUD 冗余位置写入) + +| ID | 状态 | 修改文件 | 说明 | +|---|---|---|---| +| N3 Setup 冗余位置设置 | ✅ 已修复 | `MinimapHUD.cs` | RefreshView step② 在 Instantiate+Setup 后立即调用 `PlaceCell(cell, room)` 设置正确的中心相对坐标,step③ PlaceCell 覆盖成为幂等确认操作 | + +### P3 未修复项(低优先级,架构级改动) + +| ID | 状态 | 原因 | +|---|---|---| +| P3-1 缺少 RegionSO | ⏳ 待后续 | 需新建 SO 类 + 编辑器工具 + 修改 MapManager,工作量较大 | +| P3-2 MapDesignSpec.md | ⏳ 待后续 | 文档补充,不影响运行 | +| P3-3 IMapService.Database 具体类 | ⏳ 待后续 | 需调整接口,影响范围广 | +| P3-4 MapPanel Overlay 扩展点 | ⏳ 待后续 | 架构扩展,需设计方案 | +| P3-5 GetRoomsByRegion LINQ 分配 | ⏳ 待后续 | 低频调用,影响可忽略 | + +### 修复后预估得分 + +| 维度 | 修复前 | 修复后预估 | +|---|---|---| +| 性能 | 8/10 | **8.5/10**(P1-1 空间索引修复主要瓶颈) | +| 编辑器扩展 | 8.5/10 | **9/10**(P2-1 GUIStyle + P2-3 Undo 刷新均已修复) | +| 数据设计 | 8/10 | **8/10**(ValidateAll 保护改善了构建质量,不影响功能评分) | +| 其他维度 | 不变 | 不变 | +| **总分** | **85/100** | **~87/100** | + diff --git a/Docs/Review/Minimap_Review_Round7_Independent.md b/Docs/Review/Minimap_Review_Round7_Independent.md new file mode 100644 index 0000000..886f44b --- /dev/null +++ b/Docs/Review/Minimap_Review_Round7_Independent.md @@ -0,0 +1,368 @@ +# 小地图系统 Round 7 独立再审查报告 + +> **评估日期**:Round 7 +> **评估方法**:在 Round 6 修复完成后,再次独立通读所有 17 个地图模块文件,重点发现 Round 6 遗漏的真实问题 +> **对标标准**:成熟 2D Metroidvania 游戏小地图,专注编辑器扩展质量、架构解耦、高性能、策划友好度 + +--- + +## 一、Round 6 修复验证 + +| Round 6 修复 ID | 验证结果 | 备注 | +|---|---|---| +| P1-1 MinimapHUD 空间索引 | ✅ 通过 | `_spatialIndex` 构建/查询逻辑正确,O(viewRadius²) 替代 O(N) 落实 | +| P2-1 MapDatabaseEditor GUIStyle 缓存 | ✅ 通过 | `_errorRowStyle` + `GetErrorRowStyle()` 惰性初始化,`OnEnable` 重置支持皮肤切换 | +| P2-2 MapRoomDataEditor CELL_SIZE 说明 | ✅ 通过 | Inspector HelpBox 已补充三段坐标系说明 | +| P2-3 MapLayoutEditorWindow Undo 刷新 | ✅ 通过 | `OnEnable`/`OnDisable` 正确注册/注销,`OnUndoRedo` 清除验证缓存并 Repaint | +| P2-4 ValidateAll 编辑器保护 | ✅ 通过 | `#if UNITY_EDITOR` 包裹完整,构建包不再含 O(N²) 验证逻辑 | +| N3 MinimapHUD Setup 冗余位置 | ⚠️ 部分通过 | step② 已加 PlaceCell;但 step③ 仍重新遍历全部 _cells 包含刚 PlaceCell 完的新格子,新格子被 PlaceCell 两次(详见 R7-N6) | + +Round 6 主要修复落实良好,但 N3 引入了新的小冗余。 + +--- + +## 二、Round 7 新发现的问题 + +### 🔴 **R7-N1**:MapPanel 不响应 Pin 增删事件(真实 UX 缺陷) + +**位置**:`MapPanel.cs` 第 88-92 行 + +```csharp +private void OnEnable() +{ + ... + RenderPins(); // ← 只在打开面板时调用一次 + UpdatePlayerIcon(); + CenterOnCurrentRoom(); + _onMapUpdated?.Subscribe(OnMapUpdated).AddTo(_subs); +} +``` + +**问题**: +- `RenderPins()` 仅在 `OnEnable` 调用一次 +- `LateUpdate` 只刷新玩家图标,**从不重新检查 PinsVersion** +- 玩家在地图打开状态下添加/删除 Pin(典型操作),UI 不会同步刷新 + +**影响**:地图面板期间用户的所有 Pin 操作(CreatePin/RemovePin)在面板关闭并重新打开前,视觉上无任何反馈。这是策划/玩家可见的 UX 错误。 + +**修复方案**: +- 方案 A(轻量、推荐):在 `LateUpdate` 末尾调用 `RenderPins()`,已有 `_lastPinVersion` 脏检查,无版本变化时立即 return,无开销 +- 方案 B:在 `IPinService` 中新增 `event Action OnPinsChanged`,`MapPinManager` 在 `AddPin/RemovePin` 时触发 + +--- + +### 🔴 **R7-N2**:MapPanel/MinimapHUD 不响应数据库热更(编辑器/工具集成缺陷) + +**位置**:`MapPanel.cs` 第 83-86 行,`MinimapHUD.cs` 第 56-70 行 + +```csharp +// MapPanel +if (_cells.Count == 0) + BuildGrid(); +else + RefreshAllCells(); // ← 只刷新已存在 _cells 的可见性 +``` + +```csharp +// MinimapHUD +BuildSpatialIndex(_mapSvc?.Database); // ← 仅在 OnEnable 时构建 +``` + +**问题**: +- 策划在编辑器中通过 `MapLayoutEditorWindow` 编辑房间布局或新增房间后,运行中的 MapPanel/MinimapHUD 不会反映变化 +- `RefreshAllCells` 只更新已有 `_cells` 集合,新增房间不会被实例化、被删除房间的格子不会被回收 +- 空间索引同样不会重建 + +**影响**: +- 策划开发时的迭代体验差:每次编辑数据库都需重启游戏 +- 运行时若支持 Addressable 数据库热更或动态地图生成(DLC),地图会失效 + +**修复方案**: +1. `IMapService` 新增 `event Action OnDatabaseChanged` +2. `MapDatabaseSO.OnValidate` 中触发该事件(编辑器侧) +3. `MapPanel`/`MinimapHUD` 订阅并执行完整重建 + +--- + +### 🟡 **R7-N3**:BuildSpatialIndex 在 MinimapHUD 和 MapPlayerTracker 中重复实现 + +**位置**:`MinimapHUD.cs` 第 97-108 行 vs `MapPlayerTracker.cs` 第 67-81 行 + +两处构建几乎相同的 `Dictionary` 索引(玩家追踪和小地图查询)。每个组件都独立构建一次相同数据,浪费内存 + 数据不一致风险(房间数据变化时一方更新另一方未更新)。 + +**修复方案**:将索引下沉为 `MapDatabaseSO` 的内置查询: + +```csharp +// MapDatabaseSO 中 +private Dictionary _cellToRoom; +public string GetRoomIdAtCell(Vector2Int cell) { ... } +``` + +或封装为 `MapServiceExtensions.GetRoomIdAtCell(this IMapService svc, Vector2Int cell)` 集中实现。 + +--- + +### 🟡 **R7-N4**:MapPinManager.CreatePin 缺少输入校验 + +**位置**:`MapPin.cs` 第 61-74 行 + +```csharp +public MapPin CreatePin(string roomId, float normX, float normY, + PinType type = PinType.Marker, string note = "") +{ + var pin = new MapPin { + RoomId = roomId, // 不验证非空、不验证 roomId 是否存在 + NormalizedPosX = normX, // 不 Clamp01 + NormalizedPosY = normY, + ... + }; + AddPin(pin); + return pin; +} +``` + +**问题**: +- `roomId` 可空字符串/无效 ID,导致 `MapPanel.RenderPins` 中 `TryGetValue` 失败但 Pin 仍存在于存档 +- `normX/normY` 可为负数或大于 1,Pin 显示在房间格子外 +- 错误数据持久化到存档,后续清理困难 + +**修复方案**:参数校验 + `Mathf.Clamp01` + RoomId 存在性检查(可选警告)。 + +--- + +### 🟡 **R7-N5**:MapManager.OnRoomEntered 区域变化日志缺失 + +**位置**:`MapManager.cs` 第 70-82 行 + +```csharp +private void OnRoomEntered(string roomId) +{ + bool changed = _exploredRooms.Add(roomId); + if (changed) _onMapUpdated?.Raise(roomId); + + var regionId = _database?.GetRoom(roomId)?.RegionId; + if (!string.IsNullOrEmpty(regionId) && regionId != _currentRegionId) + { + _currentRegionId = regionId; + _onRegionChanged?.Raise(regionId); + } +} +``` + +**问题**: +- 首次启动游戏(无存档加载)时,玩家首次进入某区域,`_currentRegionId` 由 null 变成 `regionId`,会触发一次 `EVT_RegionChanged` +- 但 **加载存档后**,`OnLoad` 只恢复 `_exploredRooms`/`_mappedRooms`,**不恢复 `_currentRegionId`** +- 玩家加载存档进入游戏时,第一次进入房间会再次触发 EVT_RegionChanged,导致「读档后第一次进房显示区域名渐显」UX 异常 + +**修复方案**:`OnLoad` 同步恢复 `_currentRegionId`(可基于玩家当前位置或最近进入的房间推导)。 + +--- + +### 🟡 **R7-N6**:MinimapHUD step③ 对新格子的二次 PlaceCell + +**位置**:`MinimapHUD.cs` 第 175-194 行 + +```csharp +foreach (var roomId in _roomsInViewBuffer) +{ + ... + PlaceCell(cell, room); // ← step② 中刚调用 + _cells[roomId] = cell; +} + +// ③ 重定位所有格子 +foreach (var (id, cell) in _cells) +{ + ... + PlaceCell(cell, r); // ← 新格子在此被再次 PlaceCell +} +``` + +**问题**:刚在 step② 中通过 PlaceCell 设置过位置的新格子,在 step③ 中又被 PlaceCell 一遍。虽然结果幂等,但浪费写入。 + +**修复方案**:step② 不调用 PlaceCell(信任 step③ 统一处理),或 step③ 跳过本帧新增的格子。后者更清晰: + +```csharp +var newlyAdded = new HashSet(); +// step② +foreach (var roomId in _roomsInViewBuffer) { + if (_cells.ContainsKey(roomId)) continue; + ... + PlaceCell(cell, room); + newlyAdded.Add(roomId); + _cells[roomId] = cell; +} +// step③ 重定位剩余存量格子 +foreach (var (id, cell) in _cells) { + if (newlyAdded.Contains(id)) continue; + ... +} +``` + +--- + +### 🟢 **R7-N7**:MapInputHandler 仍使用旧版 Input System(Round 6 N4 未修复) + +**位置**:`MapInputHandler.cs` 第 42-43 行 + +```csharp +float h = Input.GetAxisRaw("Horizontal"); +float v = Input.GetAxisRaw("Vertical"); +``` + +新 Input System 项目此代码失效,已在 Round 6 标注但未修复,本轮再次确认。 + +--- + +### 🟢 **R7-N8**:MapPlayerTracker._worldUnitsPerCell 字段无范围保护 + +**位置**:`MapPlayerTracker.cs` 第 25 行 + +```csharp +[SerializeField] private float _worldUnitsPerCell = 18f; +``` + +若策划误填 0 或负值,会导致: +- `WorldToCell` 除以 0 产生 `Infinity`,进而 `Mathf.FloorToInt` 异常 +- 归一化位置计算 `worldSize.x` 为 0,被 `Mathf.Max(1f, ...)` 兜底但语义错误 + +**修复方案**:`[Min(0.01f)]` 或 `OnValidate` 保护。 + +--- + +### 🟢 **R7-N9**:MapPin.cs 文件名与类名不一致(历史遗留) + +`MapPin.cs` 内部包含 `MapPinManager`(主类)+ `PinSpriteEntry`。VS Code/Rider 全局搜索 `MapPinManager` 文件名找不到,新人查找代码体验差。 + +**修复方案**:因 Unity .meta GUID 绑定限制,安全做法是新建 `MapPinManager.cs` 仅含一个 `// see MapPin.cs` 注释引导(不安全的做法是改 .meta 但风险高);或保持现状并在 README 注明。 + +--- + +### 🟢 **R7-N10**:MapDatabaseSO._index OnDisable 清理的潜在 NRE + +**位置**:`MapRoomDataSO.cs` 第 92 行 + +```csharp +private void OnDisable() => _index = null; +``` + +ScriptableObject 在域重载(Domain Reload)/编辑器停止播放时执行 OnDisable。若同时有运行中的协程或异步任务持有 SO 引用并即将调用 `GetRoom`,会触发索引重建(线性 LINQ),首帧停止时性能尖峰 + 短暂 NRE 风险。 + +**修复方案**:清理改为 `lock` 包裹的重建逻辑,或保留缓存让 GC 自然回收。当前实现风险低但非完全无害。 + +--- + +## 三、本轮独立评分 + +| 维度 | Round 6 | Round 7 独立 | 差值 | 说明 | +|---|---|---|---|---| +| 架构解耦 | 9.0 | **8.5** | -0.5 | R7-N3(索引重复实现)扣 0.5 | +| 性能 | 8.0→8.5 | **8.5** | 维持 | P1-1 已修复,新发现的 N6 冗余写入影响微小 | +| 编辑器扩展 | 8.5→9.0 | **9.0** | 维持 | P2 修复落实 | +| 数据设计 | 8.0 | **7.5** | -0.5 | R7-N4(无输入校验)+ R7-N5(OnLoad 不恢复 region)扣分 | +| 功能完整性 | 8.0 | **7.5** | -0.5 | R7-N1(Pin 不响应增删)是真实功能缺陷 | +| 代码质量 | 8.5 | **8.5** | 0 | 整体仍达标 | +| 可扩展性 | 8.0 | **7.5** | -0.5 | R7-N2(无热更事件)限制工具化扩展 | +| 策划友好度 | 8.0 | **7.5** | -0.5 | R7-N2 影响策划迭代体验 | + +### 加权总分 + +``` +架构解耦 ████████░░ 8.5/10 +性能 ████████░░ 8.5/10 +编辑器扩展 █████████░ 9.0/10 +数据设计 ███████░░░ 7.5/10 +功能完整性 ███████░░░ 7.5/10 ← R7-N1 Pin 响应缺陷 +代码质量 ████████░░ 8.5/10 +可扩展性 ███████░░░ 7.5/10 ← R7-N2 无热更 +策划友好度 ███████░░░ 7.5/10 +───────────────────────────── +加权总分 82/100 +``` + +**Round 6 报告预估修复后 87 分,本轮独立审查实际 82 分**——差距源于 Round 6 评估视野未覆盖 N1/N2/N3 这三类"运行时-编辑器"协同问题。 + +--- + +## 四、严重程度排序与修复优先级 + +### P0(立即修复,影响真实 UX/功能) + +| ID | 问题 | 工时估计 | +|---|---|---| +| R7-N1 | MapPanel 不响应 Pin 增删 | 小(LateUpdate 加 RenderPins 调用) | +| R7-N5 | OnLoad 不恢复 _currentRegionId | 小(OnLoad 推导一行) | + +### P1(高:影响工具化与扩展) + +| ID | 问题 | 工时估计 | +|---|---|---| +| R7-N2 | MapPanel/MinimapHUD 不响应数据库热更 | 中(IMapService 新增事件 + 订阅) | +| R7-N3 | BuildSpatialIndex 重复实现 | 中(下沉到 MapDatabaseSO 或扩展方法) | +| R7-N4 | CreatePin 缺输入校验 | 小(参数 Clamp + 警告) | + +### P2(中:代码质量与健壮性) + +| ID | 问题 | 工时估计 | +|---|---|---| +| R7-N6 | step③ 对新格子二次 PlaceCell | 小(newlyAdded 集合跳过) | +| R7-N7 | MapInputHandler 旧版 Input | 中(替换为 IInputService) | +| R7-N8 | _worldUnitsPerCell 无范围保护 | 极小(`[Min]`) | + +### P3(低:历史遗留 / 边缘场景) + +| ID | 问题 | +|---|---| +| R7-N9 | MapPin.cs 文件名与类名不一致 | +| R7-N10 | _index OnDisable 清理潜在 NRE | + +--- + +## 五、对标空洞骑士 / 丝之歌的差距分析 + +| 能力 | 对标标准 | 当前状态 | 差距 | +|---|---|---|---| +| **三级可见性(Unknown/Explored/Mapped)** | ✓ | ✓ 完整 | 无 | +| **MapFragment 购买揭示** | ✓ | ✓ 接口已就位 | 无 | +| **自定义 Pin 标记** | ✓ 多种类型 | ✓ 但**地图打开时不响应增删** | **N1** | +| **区域名渐显** | ✓ | ✓ 但读档后会异常触发 | **N5** | +| **存档点显示完整地图** | ✓ | ✓ via SetMapped API | 无 | +| **房间非矩形轮廓** | ✓ | ✓ RoomOutlineTex 字段 | 无 | +| **小地图视野跟随玩家** | ✓ | ✓ 含空间索引优化 | 无 | +| **图标平滑跟随(非格子跳跃)** | ✓ | ✓ Round 4 已修复 | 无 | +| **运行时缩放/平移** | ✓ 滚轮+键盘+手势 | ⚠️ 仅滚轮+键盘 | 无手势/手柄 | +| **探索进度统计 UI** | ✓ | ❌ API 完整但无 UI 显示 | 中等 | +| **区域配色主题** | ✓ 各区域有专属配色 | ❌ 编辑器随机配色,运行时无 | 大(需 RegionSO) | +| **数据库热更** | — | ❌ 不响应 | **N2** | + +--- + +## 六、总评 + +### 当前最终得分:**82/100** + +**优势**: +- 完整的接口/ServiceLocator 解耦体系 +- 编辑器三工具(SO Inspector + Custom Editor + 全局布局窗口)功能扎实 +- 热路径性能已优化(脏检查 + 缓存 + 空间索引) +- 数据驱动设计支持策划独立迭代 + +**真实差距(按修复成本排序)**: +1. **N1 Pin 不响应增删** — 1 行修复,是当前最高 ROI 的修复点 +2. **N5 OnLoad 不恢复 region** — 1 行修复,避免读档体验异常 +3. **N4 CreatePin 无校验** — 3 行修复,避免脏数据进入存档 +4. **N6 step③ 二次 PlaceCell** — 5 行修复,性能小优化 +5. **N3 索引重复实现** — 重构,DRY 改善 +6. **N2 数据库热更事件** — 中等改动,但显著提升策划工作流 + +**仍未弥补的功能缺口**(Round 5/6 已标注,仍在): +- 探索进度 UI(API 有,UI 无) +- RegionSO(区域配色/名称集中配置) +- MapDesignSpec.md 策划文档 +- Pinch 缩放、手柄输入 + +如果完成 P0+P1(N1/N5/N2/N3/N4),评分预期回到 **88-90 分**;再补 RegionSO + 进度 UI 可达 **93+**。 + +--- + +*Round 7 旨在矫正 Round 6 在「运行时-编辑器协同」「事件响应完整性」两个视角的盲区。本轮重点发现的 N1/N2/N5 是 Round 1-6 全部漏检的真实功能/UX 缺陷,建议立即修复。* diff --git a/zeling_v2.sln b/zeling_v2.sln index c4c8e60..9e4f158 100644 --- a/zeling_v2.sln +++ b/zeling_v2.sln @@ -41,6 +41,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BaseGames.Core", "BaseGames EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Assembly-CSharp-Editor", "Assembly-CSharp-Editor.csproj", "{278D6C47-C52B-D206-DB1C-429D79FFAD5A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BaseGames.World.Map", "BaseGames.World.Map.csproj", "{16BB97E7-3EA9-4707-2D93-441D9C908404}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Assembly-CSharp", "Assembly-CSharp.csproj", "{BDE6E0A0-CE2D-39A5-53EB-DCA516DEF547}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BaseGames.Player.States", "BaseGames.Player.States.csproj", "{137EBC35-6D54-8E27-0DF7-C9F5F0E63705}" @@ -71,8 +73,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BaseGames.Camera", "BaseGam EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kybernetik.Animancer.FSM", "Kybernetik.Animancer.FSM.csproj", "{54A35301-41E5-2524-BFA0-B5B1B9B2BCD9}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BaseGames.World.Map", "BaseGames.World.Map.csproj", "{16BB97E7-3EA9-4707-2D93-441D9C908404}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BaseGames.Support", "BaseGames.Support.csproj", "{9D493EDA-C9BA-CB3D-2911-F7CEC16EC9B3}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Opsive.GraphDesigner.Runtime.Wrappers", "Opsive.GraphDesigner.Runtime.Wrappers.csproj", "{06C93DFC-ACB7-5B27-C63F-7878F54D61DA}" @@ -209,6 +209,10 @@ Global {278D6C47-C52B-D206-DB1C-429D79FFAD5A}.Debug|Any CPU.Build.0 = Debug|Any CPU {278D6C47-C52B-D206-DB1C-429D79FFAD5A}.Release|Any CPU.ActiveCfg = Release|Any CPU {278D6C47-C52B-D206-DB1C-429D79FFAD5A}.Release|Any CPU.Build.0 = Release|Any CPU + {16BB97E7-3EA9-4707-2D93-441D9C908404}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {16BB97E7-3EA9-4707-2D93-441D9C908404}.Debug|Any CPU.Build.0 = Debug|Any CPU + {16BB97E7-3EA9-4707-2D93-441D9C908404}.Release|Any CPU.ActiveCfg = Release|Any CPU + {16BB97E7-3EA9-4707-2D93-441D9C908404}.Release|Any CPU.Build.0 = Release|Any CPU {BDE6E0A0-CE2D-39A5-53EB-DCA516DEF547}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BDE6E0A0-CE2D-39A5-53EB-DCA516DEF547}.Debug|Any CPU.Build.0 = Debug|Any CPU {BDE6E0A0-CE2D-39A5-53EB-DCA516DEF547}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -269,10 +273,6 @@ Global {54A35301-41E5-2524-BFA0-B5B1B9B2BCD9}.Debug|Any CPU.Build.0 = Debug|Any CPU {54A35301-41E5-2524-BFA0-B5B1B9B2BCD9}.Release|Any CPU.ActiveCfg = Release|Any CPU {54A35301-41E5-2524-BFA0-B5B1B9B2BCD9}.Release|Any CPU.Build.0 = Release|Any CPU - {16BB97E7-3EA9-4707-2D93-441D9C908404}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {16BB97E7-3EA9-4707-2D93-441D9C908404}.Debug|Any CPU.Build.0 = Debug|Any CPU - {16BB97E7-3EA9-4707-2D93-441D9C908404}.Release|Any CPU.ActiveCfg = Release|Any CPU - {16BB97E7-3EA9-4707-2D93-441D9C908404}.Release|Any CPU.Build.0 = Release|Any CPU {9D493EDA-C9BA-CB3D-2911-F7CEC16EC9B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9D493EDA-C9BA-CB3D-2911-F7CEC16EC9B3}.Debug|Any CPU.Build.0 = Debug|Any CPU {9D493EDA-C9BA-CB3D-2911-F7CEC16EC9B3}.Release|Any CPU.ActiveCfg = Release|Any CPU