# 小地图系统 Round 11 独立评估报告 > 评估时间:2026-05-25 > 基准版本:R10 全部修复落地后当前 HEAD > 评估范围:`Assets/_Game/Scripts/World/Map/` + `Assets/_Game/Scripts/Editor/World/Map/` > 对标标准:成熟 2D 银河恶魔城游戏标准(高性能、高解耦、策划友好、编辑器一流) --- ## 第 1 章:整体评分 | 维度 | 分值(满 10) | 较 R10 | |---|---|---| | 架构解耦 | 8.5 | ↑0.5(事件语义分离完成) | | 数据设计 | 8.5 | ↑0(稳定) | | 运行时性能 | 8.5 | ↑0.3(Pin 池 + Cell 保留落地) | | 编辑器扩展 | 8.0 | ↑0.5(拖拽冲突可视化、IsDefault) | | 策划友好性 | 7.5 | ↑0(仍缺 DisplayName 本地化) | | 功能完整性 | 8.5 | ↑0(稳定) | | 鲁棒性 | 7.5 | ↓0.5(发现 N11 部分订阅 Bug) | | 可扩展性 | 8.5 | ↑0.3(SetMappedBatch、OnRoomMapped) | **加权综合得分:85.6 / 100(B+)** > 注:R10 修复整体质量优秀;本轮发现 N1 MinimapHUD 部分订阅 Bug(P1 级别真实缺陷),导致鲁棒性维度扣分,综合分低于 R10 预估的 92 分。 --- ## 第 2 章:系统亮点 ### 2.1 接口与事件设计(9/10) - `IMapService` 完整定义了三个语义明确的事件:`OnDatabaseChanged`(结构变更)/ `OnExplorationChanged`(探索进度)/ `OnRoomMapped`(单房间解锁)。 - 消费方(MapPanel、MinimapHUD)通过接口与 ServiceLocator 完全解耦,不持有任何具体 MonoBehaviour 引用。 - `MapServiceExtensions.GetVisibility` 集中三级可见性推导逻辑,避免分散重复。 ### 2.2 空间索引共享(9/10) - `MapDatabaseSO.GetRoomIdAtCell(Vector2Int)` 惰性构建一次,供 `MapPlayerTracker` / `MinimapHUD.RefreshView` 共享,O(1) 格子查找。 - `InvalidateIndex` 在结构变更时统一失效,不存在缓存过期风险。 ### 2.3 MinimapHUD 增量刷新(8.5/10) - `RefreshView` 为 O(viewRadius²) 而非 O(allRooms),大地图下效果显著。 - 回收/新建格子避免全量重建,`_toRemove` / `_roomsInViewBuffer` 列表复用消除高频 GC。 ### 2.4 编辑器工具套件(8/10) - `MapLayoutEditorWindow`:格子布局预览 + 区域着色 + 拖拽移房 + 冲突可视化(R10-N5)+ 搜索/图例 + 验证 + Play Mode 玩家位置。 - `MapRoomDataEditor`:Scene View 双角控制点直接拖拽,策划可在场景中直观编辑房间尺寸。 - `MapRoomAutoRegister`:新建 SO 自动追加到默认 Database,消灭策划忘记注册的问题。 ### 2.5 数据兼容性保障(9/10) - `[FormerlySerializedAs("AllRooms")]` 确保 `_allRooms` 字段重命名后现有 `.asset` 不丢失数据。 - `EditorSetRooms` 专用写入器防止外部代码绕过封装直接赋值。 --- ## 第 3 章:新发现问题(R11-N1 ~ N12) ### R11-N1 ★P1★ — MinimapHUD `_subscribed` 标志导致部分订阅场景下事件永不触发 **文件:** `MinimapHUD.cs` → `SubscribeServices()` **现象:** ```csharp private void SubscribeServices() { _mapSvc ??= ServiceLocator.GetOrDefault(); _playerProvider ??= ServiceLocator.GetOrDefault(); _pinService ??= ServiceLocator.GetOrDefault(); if (_subscribed) return; // ← 提前 return 阻断后续 if (_mapSvc == null && _playerProvider == null) return; if (_playerProvider != null) _playerProvider.OnRoomChanged += OnRoomChanged; if (_mapSvc != null) { _mapSvc.OnDatabaseChanged += OnDatabaseChanged; _mapSvc.OnExplorationChanged += OnExplorationChanged; } _subscribed = true; // ← 仅当上方至少一个服务非 null 时才置位 } ``` **具体 Bug:** 场景——`_playerProvider` 在 Awake 时已注册(优先 ExecutionOrder),但 `_mapSvc` 尚未就绪: 1. 第一次调用:`_playerProvider` 成功,`_mapSvc == null` → 仅订阅 `OnRoomChanged`,置 `_subscribed = true`。 2. 后续调用:`_mapSvc` 现已就绪,但 `if (_subscribed) return` 提前退出,**`OnDatabaseChanged` / `OnExplorationChanged` 永远不订阅**。 3. 结果:小地图 HUD 读档后不刷新、房间解锁后不更新颜色。 **修复方案:** 改为分别追踪 `_mapSvcSubscribed` / `_playerSubscribed`,或直接仿照 `MapPanel` 的模式(每个服务独立 `if (svc == null)` 守门)。 --- ### R11-N2 ★P1★ — `MapRoomDataSO.OnValidate` 重复向 `delayCall` 追加委托 **文件:** `MapRoomDataSO.cs` → `OnValidate()` ```csharp private void OnValidate() { GridSize = new Vector2Int(Mathf.Max(1, GridSize.x), Mathf.Max(1, GridSize.y)); #if UNITY_EDITOR UnityEditor.EditorApplication.delayCall += NotifyOwningDatabases; // ← 问题所在 #endif } ``` **问题:** `delayCall` 是多播委托(`+=`)。当策划在 Inspector 中快速拖动滑条时,`OnValidate` 每帧调用一次,`NotifyOwningDatabases` 被追加数十次。该方法内部执行 `FindAssets` + `LoadAssetAtPath`(昂贵),会在下一帧批量执行导致卡顿。 **修复方案:** 先 `-=` 再 `+=`,保证同一 delayCall 序列中最多一次: ```csharp EditorApplication.delayCall -= NotifyOwningDatabases; EditorApplication.delayCall += NotifyOwningDatabases; ``` --- ### R11-N3 ★P1★ — `MapPinManager.OnLoad` 直接赋值反序列化 List,共享 SaveData 引用 **文件:** `MapPin.cs` → `MapPinManager.OnLoad()` ```csharp public void OnLoad(SaveData data) { _pins = data.Map.Pins ?? new List(); // ← 直接赋值,不是拷贝 PinsVersion++; } ``` `OnSave` 做了防御性拷贝(`new List(_pins)`),但 `OnLoad` 反方向没有拷贝。若调用方在 `OnLoad` 后继续持有 `data` 并修改 `data.Map.Pins`,会污染 `_pins`。 **修复:** ```csharp _pins = data.Map.Pins != null ? new List(data.Map.Pins) : new List(); ``` --- ### R11-N4 ★P1★ — `MapPanel.CenterOnCurrentRoom` 对整个 content 节点调用 `ForceRebuildLayoutImmediate` **文件:** `MapPanel.cs` → `CenterOnCurrentRoom()` ```csharp LayoutRebuilder.ForceRebuildLayoutImmediate(_scrollRect.content); ``` `ForceRebuildLayoutImmediate` 会递归重建参数节点及其所有子节点的 Layout。`_scrollRect.content` 下有所有 MapRoomCellUI 实例,重建代价随房间数线性增长(N 房间 = N 次 layout 计算)。面板每次 `OnEnable` 时执行一次,常规用法中可接受;但若项目规模扩展到 200+ 房间,此处会成为明显延迟点。 **建议:** 只有在 content 布局确实发生变化时(BuildGrid 之后)才 ForceRebuild;若 ScrollRect 没有使用 LayoutGroup,可改为直接计算 normalizedPosition,完全跳过 ForceRebuildLayoutImmediate。 --- ### R11-N5 ★P2★ — `MinimapHUD` 对 `MapRoomCellUI` 无对象池,跨房间时 GC 抖动 **文件:** `MinimapHUD.cs` → `RefreshView()` → cell 回收段 ```csharp if (cell != null) Destroy(cell.gameObject); // ← 销毁而非入池 ``` MinimapHUD 的 `RefreshView` 在玩家跨越房间边界时,会销毁视野外的 `MapRoomCellUI` GameObject 并重新实例化新进入视野的格子。 - 典型场景(走廊穿梭):每次房间切换约销毁/创建 3-8 个 Cell GameObject,频率可达 1-2 次/秒。 - Pin 已有对象池,但 Cell 没有,导致一定 GC 压力。 **建议:** 为 `MapRoomCellUI` 建立 `Stack _cellPool`,回收时 `SetActive(false)` 入池,需要时出池重置,与 Pin 池保持一致。 --- ### R11-N6 ★P2★ — `MapManager.GetRoomsByRegion` 每次调用都分配新数组 **文件:** `MapManager.cs` ```csharp public MapRoomDataSO[] GetRoomsByRegion(string regionId) => _database.AllRooms.Where(r => r != null && r.RegionId == regionId).ToArray(); ``` 每次调用分配 LINQ 枚举器 + 结果数组。若调用方(如 MapPanel 地区筛选、成就系统)在 Update 中使用,会造成 GC。 **建议:** 加结果缓存(Dictionary),在 `NotifyDatabaseChanged` 时失效。 --- ### R11-N7 ★P2★ — `MapLayoutEditorWindow` 不监听外部资产变更 **文件:** `MapLayoutEditorWindow.cs` 窗口打开后: - 若从代码/其他窗口修改 `MapDatabaseSO`(如 MapDatabaseEditor 的 Validate 按钮),布局窗口不自动刷新,需用户手动交互。 - `Undo.undoRedoPerformed` 正确注册,但外部变更(`EditorUtility.SetDirty` 后保存、脚本修改资产)不触发 `Repaint`。 **建议:** 监听 `EditorApplication.projectWindowItemOnGUI` 或使用 `AssetDatabase.postprocessAllAssets`;或在 `OnGUI` 开头检查 database 的 `AllRooms` 数组引用变化(版本号方案)。 --- ### R11-N8 ★P2★ — `MapRoomCellUI.Setup` 的 `pixelsPerCell` 参数对 MinimapHUD 调用路径存在 API 语义歧义 **文件:** `MapRoomCellUI.cs` / `MinimapHUD.cs` ```csharp // MinimapHUD 调用路径: cell.Setup(room, _mapSvc.GetVisibility(room.RoomId), null); // 使用默认 pixelsPerCell=32 cell.SetColors(_colorExplored, _colorMapped, _colorUnknown); PlaceCell(cell, room); // 立即覆盖 RT.anchoredPosition 和 sizeDelta ``` `Setup` 内部已根据 `pixelsPerCell` 计算并写入了 `RT.anchoredPosition` / `RT.sizeDelta`,但 MinimapHUD 立即用 `PlaceCell` 覆盖,造成无意义的写入。`pixelsPerCell` 参数对 MinimapHUD 路径无实际效果,但 API 签名暗示它有意义,容易误导维护者。 **建议:** 在 `Setup` 中将位置/尺寸计算提取为 `SetGridLayout(room, pixelsPerCell)` 方法,MinimapHUD 调用 `Setup` 时不传位置参数,由 `PlaceCell` 统一负责布局。或简化为重载:`Setup(room, visibility, icon)` + `Setup(room, visibility, icon, pixelsPerCell)`。 --- ### R11-N9 ★P2★ — `MapPlayerTracker` 假设世界原点与格子原点重合,无 WorldOffset 参数 **文件:** `MapPlayerTracker.cs` ```csharp private Vector2Int WorldToCell(Vector2 worldPos) => new(Mathf.FloorToInt(worldPos.x / _worldUnitsPerCell), Mathf.FloorToInt(worldPos.y / _worldUnitsPerCell)); ``` 如果关卡世界坐标原点不在 (0,0)(如整个世界在 Y=-500 以下),此计算会得到错误的格子坐标,导致玩家位置追踪完全失效。 **建议:** 增加 `[SerializeField] private Vector2 _worldOriginOffset` 字段,`WorldToCell` 先减去 `_worldOriginOffset` 再除以 `_worldUnitsPerCell`。 --- ### R11-N10 ★P2★ — `MapLayoutEditorWindow.DrawExitLines` 连线使用房间中心而非实际出口格子坐标 **文件:** `MapLayoutEditorWindow.cs` → `DrawExitLines()` ```csharp Vector2 from = GridCenterToClip(room.GridPosition + room.GridSize / 2, origin); // 房间中心 Vector2 to = GridCenterToClip(target.GridPosition + target.GridSize / 2, origin); ``` `RoomExitData` 结构中已有 `ExitGridPos` 字段(出口在格子地图上的实际位置),但 `DrawExitLines` 画的是两个房间**中心**之间的连线。对于大尺寸房间,连线起止点可能距离实际出口较远,策划无法直观判断出口对齐情况。 **建议:** 改为从 `exit.ExitGridPos` 到对应 target 房间的对应出口格子坐标,若 target 无对应出口则退化为中心连线。 --- ### R11-N11 ★P2★ — `MapRoomDataSO` 公共字段缺少 RoomId 命名规则验证 **文件:** `MapRoomDataSO.cs` / `MapDatabaseSO.cs` → `ValidateAll()` `RoomId` 字段直接用于: 1. 场景名匹配(`OnRoomEntered` 事件传入场景名) 2. Dictionary key 查找 3. 存档 HashSet 存储 目前 `ValidateAll` 检查了重复和空值,但未检查: - 首尾空格(`" Room_A "` 与 `"Room_A"` 被视为不同但功能等效时易混淆) - 特殊字符(`/`、`\` 等可能影响路径处理的字符) **建议:** 在 `MapRoomDataSO.OnValidate` 中自动 `Trim()`;在 `ValidateAll` 中增加含空格/特殊字符的警告。 --- ### R11-N12 ★P3★ — `MapLayoutEditorWindow.DrawExitLines` 连线在极端缩放(≤ 0°)时 `GUI.matrix` 未正确恢复 **文件:** `MapLayoutEditorWindow.cs` → `DrawLine()` ```csharp GUIUtility.RotateAroundPivot(angle, mid); GUI.DrawTexture(...); GUI.matrix = prevMatrix; // 手动恢复 ``` 若 `DrawTexture` 抛出异常(如纹理被意外卸载),`GUI.matrix` 不会被恢复,导致整个窗口绘制出现旋转偏移。 **建议:** 使用 `using (new GUIMatrixScope(...))` 或 `try/finally` 包裹: ```csharp var prev = GUI.matrix; try { GUIUtility.RotateAroundPivot(angle, mid); GUI.DrawTexture(...); } finally { GUI.matrix = prev; } ``` --- ## 第 4 章:维度详细评分 ### 4.1 架构解耦 — 8.5/10 **优秀:** - IMapService 接口三个独立事件,语义清晰 - ServiceLocator 注册在 Awake/OnDestroy,生命周期正确 - MapPinManager 独立于 MapManager,通过 IPinService 解耦 - MapServiceExtensions 扩展方法集中可复用逻辑 **不足:** - MinimapHUD `_subscribed` 标志存在部分订阅 Bug(N1 P1) - MapPanel 仍通过 `StringEventChannelSO _onMapUpdated` 双通道接收单房间更新(OnMapUpdated + OnExplorationChanged 语义重叠但各有其用,轻微冗余) --- ### 4.2 数据设计 — 8.5/10 **优秀:** - 三级可见性(Unknown / Explored / Mapped)精确匹配银河恶魔城标准 - MapDatabaseSO 懒构建双索引(id → data,cell → roomId),共享给所有消费方 - MapRoomDataSO.OnValidate 自动修正 GridSize 最小值 - RoomExitData 包含 TransitionType,为场景切换类型扩展预留 **不足:** - GetRoomsByRegion 无结果缓存(N6 P2) - DisplayName 无 i18n 路径(延续 R10-N10) - RoomId 无命名规则强制检查(N11 P2) --- ### 4.3 运行时性能 — 8.5/10 **优秀:** - MinimapHUD RefreshView O(viewRadius²),大地图下远快于 O(N) - Pin 对象池(MapPanel + MinimapHUD),ClearPins = SetActive(false) 而非 Destroy - 脏标志驱动 UI(databaseDirty / explorationDirty / viewDirty) - LateUpdate 双重脏检查(PinsVersion + 玩家位置) - MapDatabaseSO 空间索引 O(1) 哈希查找 **不足:** - MinimapHUD MapRoomCellUI 无对象池(N5 P2),跨房间边界 GC 抖动 - CenterOnCurrentRoom 对 content ForceRebuildLayoutImmediate(N4 P1),大房间数时开销可见 - GetRoomsByRegion LINQ.ToArray() 无缓存(N6 P2) --- ### 4.4 编辑器扩展 — 8.0/10 **优秀:** - MapLayoutEditorWindow 全功能:zoom/pan/drag/conflict/search/legend/validate/PlayMode 玩家点 - MapRoomDataEditor Scene View 双角控制点 + 吸附 + Undo + 居中快捷键 - MapDatabaseEditor 一键验证 + 房间列表 + 错误行红色高亮 - MapRoomAutoRegister 自动注册消除遗漏风险 + EditorPrefs 开关 - Undo/Redo 刷新支持 **不足:** - 外部资产变更不触发窗口刷新(N7 P2) - DrawExitLines 用中心连线而非实际出口格坐标(N10 P2) - OnValidate delayCall 重复追加(N2 P1) --- ### 4.5 策划友好性 — 7.5/10 **优秀:** - 布局编辑器拖拽房间 + 冲突立即变红,无需专业编程知识 - 搜索 + 图例 + 区域着色帮助大地图快速定位 - 自动注册新房间无需手动维护 Database - Play Mode 实时玩家位置可视化 **不足:** - 出口连线视觉不够精确(N10),策划无法确认出口对齐 - 无键盘快捷键(如 V = 验证,F = 重置视图) - 无批量移动/对齐多个房间能力(延续 R10-N6) - DisplayName 无法本地化预览(延续 R10-N10) --- ### 4.6 功能完整性 — 8.5/10 **优秀:** - 全屏地图 + 角落小地图双 UI,与头部游戏相同配置 - SetMappedBatch 支持地图碎片批量解锁 - OnRoomMapped + OnRoomMappedAnim 虚钩子,解锁动画预留 - 探索进度 API(GetExplorationProgress / ExploredRoomCount) - MapExplorationCondition 接入成就系统 - 三种特殊房间标记(Boss / SavePoint / Shop)+ MapIconOverride 自定义 - 房间出口数据 + 过渡类型 **不足:** - 无"全地图揭示"调试命令(开发阶段常用) - 无地图房间分组/层级(如地下层 / 地面层分图) - RoomOutlineTex 非矩形形状支持存在(字段已有),但编辑器无预览 --- ### 4.7 鲁棒性 — 7.5/10 **优秀:** - MapManager / MapPlayerTracker 重复实例 Awake 检测 + _isDuplicate 守门 - 全量 null 守卫(空 Database / 空房间数组) - FormerlySerializedAs 数据兼容 - ValidateAll 四类错误检测(null / 重复 ID / 格子重叠 / 出口悬空) - _exploredRooms / _mappedRooms 使用 HashSet 防重复 **严重不足:** - MinimapHUD 部分订阅 Bug(N1 P1):`OnDatabaseChanged` / `OnExplorationChanged` 在特定启动顺序下永不触发 - MapPinManager.OnLoad 共享 SaveData 列表引用(N3 P1) --- ### 4.8 可扩展性 — 8.5/10 **优秀:** - IMapService 接口易于 Mock/测试替换 - SetMappedBatch + OnRoomMapped 为地图碎片系统提供一流扩展点 - protected virtual OnRoomMappedAnim 供 UI 子类实现动画 - RoomExitData.PreferredTransitionType 枚举为未来过渡系统预留 - MapServiceExtensions 扩展方法模式 - IsDefault 标志 + AutoRegister 支持多 Database 项目 **不足:** - 无 `IMapService.GetAllMappedRooms()` / `GetAllExploredRooms()` 返回快照 API(存档分析/成就系统需多次 HashSet 枚举) - MapRoomDataSO 无版本号字段(热更/DLC 房间 ID 变更无法追踪遗留数据) --- ## 第 5 章:优先级修复清单 ### P1 — 必须修复(影响正确性) | 编号 | 位置 | 问题摘要 | 预估工时 | |---|---|---|---| | R11-N1 | `MinimapHUD.SubscribeServices` | `_subscribed` 阻止部分订阅后续补全 → mapSvc 事件永不触发 | 1h | | R11-N2 | `MapRoomDataSO.OnValidate` | `delayCall +=` 重复追加 → 批量编辑时 N×FindAssets 卡顿 | 0.5h | | R11-N3 | `MapPinManager.OnLoad` | 直接赋值反序列化 List → 引用共享污染 SaveData | 0.5h | | R11-N4 | `MapPanel.CenterOnCurrentRoom` | `ForceRebuildLayoutImmediate(content)` → 大房间数时 OnEnable 卡顿 | 1h | ### P2 — 应当修复(影响体验/维护) | 编号 | 位置 | 问题摘要 | |---|---|---| | R11-N5 | `MinimapHUD.RefreshView` | MapRoomCellUI 无对象池,跨房间 GC 抖动 | | R11-N6 | `MapManager.GetRoomsByRegion` | LINQ ToArray() 无缓存 | | R11-N7 | `MapLayoutEditorWindow` | 外部资产变更不触发 Repaint | | R11-N8 | `MapRoomCellUI.Setup` | `pixelsPerCell` 参数对 MinimapHUD 路径无意义,API 歧义 | | R11-N9 | `MapPlayerTracker` | 无 WorldOriginOffset,世界坐标偏移场景无法使用 | | R11-N10 | `MapLayoutEditorWindow.DrawExitLines` | 中心连线而非出口格坐标,视觉精度低 | | R11-N11 | `MapRoomDataSO.OnValidate` + `ValidateAll` | RoomId 无命名规则检查(Trim / 空格 / 特殊字符) | ### P3 — 可选优化 | 编号 | 问题摘要 | |---|---| | R11-N12 | `DrawLine` GUI.matrix 未在异常路径下恢复 | --- ## 第 6 章:与标杆游戏对比 | 特性 | 本系统 | 业界标杆 | |---|---|---| | 三级可见性 | ✅ Unknown / Explored / Mapped | ✅ 标准配置 | | 角落小地图 | ✅ 视野半径可配置,增量刷新 | ✅ | | 全屏地图 + ScrollRect 居中 | ✅ | ✅ | | 地图碎片批量解锁 | ✅ SetMappedBatch | ✅(商店购买/触碰标牌解锁) | | 地图标记(Pin)系统 | ✅ 多类型 + 存档 | ✅ | | 非矩形房间形状 | ⚠️ 字段预留,编辑器无预览 | ✅(精细多边形遮罩) | | 多区域地图(分图) | ❌ | ✅(地下/地表/秘境分区) | | 房间Tooltip/命名 | ✅ DisplayName | ✅(带区域名动画) | | 键盘导航地图 | ✅(WASD/方向键) | ✅ | | 出口连接可视化 | ⚠️ 编辑器中心连线,运行时无连线 | ✅(点状通道指示) | | 地图缩放(运行时) | ✅ 滚轮缩放 | ✅ | | 地图揭示动画 | ⚠️ 钩子已预留,动画未实现 | ✅(逐格展开) | --- ## 第 7 章:总结 本系统在 R10 修复落地后已达到**商业级银河恶魔城地图系统的主体功能**,架构理念(接口 + ServiceLocator + 事件分离 + 脏标志)、编辑器工具套件(三窗口协同 + 自动注册)处于同类独立游戏工具的**前列水平**。 本轮发现的最高优先级问题集中在**鲁棒性细节**(MinimapHUD 部分订阅 Bug、OnValidate delayCall 堆积)和**API 设计细节**(Setup 参数歧义、GetRoomsByRegion 分配),修复这些问题后综合评分预估可恢复至 **90~91 / 100(A-)**。 长期来看,补齐以下能力可冲击 95/100(A): 1. MapRoomCellUI 对象池(N5) 2. 多区域/分图支持 3. 非矩形房间轮廓编辑器预览 4. 出口精确连线可视化(N10) 5. WorldOriginOffset 参数(N9) --- *本报告独立于前序轮次评审,基于 2026-05-25 当前代码库完整重读后生成。*