# 小地图系统独立评审 Round 21 **评审时间**:R21(基于 R20 全部修复已落地的代码基线) **评审范围**:`Assets/_Game/Scripts/World/Map/` 全部 19 个运行时文件 + 4 个编辑器扩展文件 **评分基准**:专业 2D Metroidvania 编辑器扩展标准(架构解耦 / 高性能 / 可扩展 / 开发者友好) --- ## R20 修复确认 | 编号 | 内容 | 状态 | |------|------|------| | R20-N1 | `RunRevealAnim` 包装协程,完成后 `_revealCoroutines.Remove(roomId)` | ✅ 确认(L214-218) | | R20-N2 | `ChooseDisplayIcon` 集中到 `MapRoomDataSO`;MapPanel/MinimapHUD 改单行委托 | ✅ 确认(L61-69 / L478-480 / L367-368) | --- ## 各维度评分 ### 1. 架构设计(Architecture) 18.5 / 20 **亮点** - ServiceLocator + 4 接口层(IMapService / IPinService / IPlayerPositionProvider / ITeleportService)完全解耦 - 事件驱动(C# Action),无轮询 - ISaveable 三处防御性拷贝对称实现 - MapRoomCellUI 在 MapPanel / MinimapHUD 共享,零重复 Prefab - `ChooseDisplayIcon` R20-N2 后唯一入口,无漂移风险 **新发现问题 N1(中等严重)**:`MinimapHUD` 缺少 LateUpdate 服务重试机制 `MapPanel.LateUpdate` 对未就绪的服务执行重试: ```csharp // MapPanel.cs L169-170 if (!_servicesReady) SubscribeServices(); ``` `MinimapHUD` 仅在 `Awake` 和 `OnEnable` 中调用 `SubscribeServices()`,**无 LateUpdate 重试**。 `MapPlayerTracker`(IPlayerPositionProvider)无 `[DefaultExecutionOrder]`,初始化顺序不确定: - 若 `MinimapHUD.Awake` 先于 `MapPlayerTracker.Awake` 执行 → `_playerProvider` 永久为 null - 后果:玩家圆点不渲染,`OnRoomChanged` 永不触发,HUD 完全静止 **扣分:−1.5** --- ### 2. 性能(Performance) 19.5 / 20 **亮点** - MapPanel 3 对象池(Cell / Pin / Exit),MinimapHUD 2 对象池(Cell / Pin) - O(viewRadius²) 空间索引查询(共享 `MapDatabaseSO._cellToRoom`) - `_servicesReady` 短路(MapPanel),消除每帧 ServiceLocator 查询 - `_revealCoroutines` 自清理(R20-N1) - 复用缓冲区:`_toRemove` / `_roomsInViewBuffer` / `_newlyAddedBuffer` - PinsVersion 脏检查 + 玩家位置脏检查 - `_regionCache` 懒加载,避免 LINQ 全扫 **说明**(信息级,不扣分):`MapPanel.BuildGrid` 末尾调用 `LayoutRebuilder.ForceRebuildLayoutImmediate`。 若 `_scrollRect.content` 含 ContentSizeFitter,此调用是必要的(确保 `CenterOnCurrentRoom` 读到正确 content 尺寸);无 LayoutGroup 时代价极低。设计合理。 **扣分:−0.5**(MinimapHUD 无 _servicesReady 短路,每次 OnEnable 均执行三次 ServiceLocator 查询) --- ### 3. 代码质量(Code Quality) 18.5 / 20 **亮点** - `CurrentZoom` 属性(R19-N1)消除双份状态 - `RunRevealAnim` 自清理协程(R20-N1) - `ChooseDisplayIcon` 单一职责集中(R20-N2) - OnValidate RoomFlags 迁移兼容 - `HasCustomExitPos` 语义布尔替代哨兵值 - `[Obsolete]` + `[HideInInspector]` 废弃字段注解完整 **新发现问题 N2(轻微)**:`MapPanel` 集合字段缺少 `readonly` 修饰 `MinimapHUD` 同类字段已标 `readonly`,两者不一致: ```csharp // MinimapHUD.cs(已正确标注) private readonly Dictionary _cells = new(); private readonly List _pinImages = new(); private readonly Stack _pinPool = new(); private readonly Stack _cellPool = new(); // MapPanel.cs(缺少 readonly,可被意外重新赋值) private Dictionary _cells = new(); private List _pinImages = new(); private Stack _pinPool = new(); private Stack _cellPool = new(); private Stack _exitPool = new(); private List _exitImages= new(); ``` `_revealCoroutines` 已正确标 `readonly`,其余集合字段漏标。 **扣分:−1.0**(N2 readonly 不一致)+ **−0.5**(MapInputHandler `_zoom` 在 `OnScroll` 仍作为局部累加器,与 `_zoomTarget.localScale.x` 功能重叠,配置错误时存在漂移风险) --- ### 4. 编辑器扩展(Editor Tools) 14.5 / 15 **亮点** - `MapLayoutEditorWindow`:缩放/平移/拖拽/搜索/区域着色/验证/Undo/热改 - `_cachedZoomForStyle` 脏检查,Style 不在每帧重建 - `_noResultStyle` 首次初始化缓存(R18-N2) - `MapDatabaseEditor`、`MapRoomDataEditor`、`MapRoomAutoRegister` 覆盖完整工作流 - `DrawExitLines` 去重缓存(`_drawnExitPairs`) **扣分:−0.5**(信息级:`MapLayoutEditorWindow` 无单元测试覆盖) --- ### 5. 功能完整性(Feature Completeness) 14.5 / 15 **亮点** - 三级可见性(Unknown / Explored / Mapped)完整实现 - 类型图标优先级(Override > SavePoint > Boss > Shop > Teleport) - Pin 系统(持久化、类型化、可视半径内渲染) - 出口连接线 + Fallback 位置(R13-N1) - 区域检测与 RegionChanged 事件 - 存档/读档(ISaveable) - 房间发现动画(RevealAnim 自清理) - 全屏地图 + 角落 HUD 双视图 **N1 影响**:若 MinimapHUD 初始化顺序不利,玩家圆点和 HUD 响应将缺失,属功能可靠性问题。**扣分:−0.5** --- ### 6. 输入系统(Input System) 9.5 / 10 **亮点** - 全部使用 InputSystem(InputReaderSO) - `MapInputHandler`:Navigate / MapCenter / OnScroll - `MinimapInputHandler`:CycleMinimapZoom - OnEnable/OnDisable 对称订阅/取消 **扣分:−0.5**(信息级:`MapInputHandler._zoom` 与 `_panel.CurrentZoom` 职责轻微重叠) --- ## 综合评分 | 维度 | 满分 | 得分 | |------|------|------| | 架构设计 | 20 | 18.5 | | 性能 | 20 | 19.5 | | 代码质量 | 20 | 18.5 | | 编辑器扩展 | 15 | 14.5 | | 功能完整性 | 15 | 14.5 | | 输入系统 | 10 | 9.5 | | **合计** | **100** | **95.0** | > 注:R21 较 R20(94.2)略有提升,主要因 R20 修复全部落地确认; > N1(服务重试缺失)是本轮最高优先级问题。 --- ## 问题清单与修复建议 ### N1 — MinimapHUD 缺少服务重试(高优先级) **根因**:`MapPlayerTracker` 无 `[DefaultExecutionOrder]`,可能晚于 `MinimapHUD` 注册服务。 **修复方案 A(推荐,防御性最强)**:在 `MinimapHUD.LateUpdate` 中补充重试。 ```csharp // MinimapHUD.cs private bool _servicesReady; private void LateUpdate() { if (!_servicesReady) SubscribeServices(); UpdatePlayerDot(); RenderPinsIfDirty(); } private void SubscribeServices() { // 原有逻辑不变,末尾加: if (_playerProvider != null && _mapSvc != null && _pinService != null) _servicesReady = true; } ``` **修复方案 B(互补)**:给 `MapPlayerTracker` 加执行顺序,确保早于无顺序 MonoBehaviour 注册。 ```csharp [DefaultExecutionOrder(-600)] // 晚于 MapManager(-700),早于默认 0 public class MapPlayerTracker : MonoBehaviour, IPlayerPositionProvider ``` **建议两个方案同时实施**:B 保证常规路径,A 防御异常路径。 --- ### N2 — MapPanel 集合字段缺少 `readonly`(低优先级) **修复**:在 `MapPanel.cs` 的集合字段声明处补充 `readonly`。 ```csharp private readonly Dictionary _cells = new(); private readonly List _pinImages = new(); private readonly Stack _pinPool = new(); private readonly Stack _cellPool = new(); private readonly Stack _exitPool = new(); private readonly List _exitImages= new(); ``` --- ### N3 — MinimapHUD.OnDisable 未重置 `_servicesReady`(伴随 N1 修复引入) 实施 N1 修复后,若 `_servicesReady` 不在 `UnsubscribeServices` / `OnDestroy` 时重置,销毁后重建场景的实例将跳过重订阅。 ```csharp private void UnsubscribeServices() { _servicesReady = false; // 补充 // 其余原有逻辑不变 } ``` --- ## 评分历史 | 轮次 | 评分 | 关键改动 | |------|------|----------| | R14 | 92.3 | — | | R15 | 91.3 | — | | R16 | 92.5 | — | | R17 | 93.0 | — | | R18 | 93.8 | MinimapHUD 废弃 _onMapUpdated 订阅 | | R19 | 93.8 | CurrentZoom 属性;_revealCoroutines 防泄漏 | | R20 | 94.2 | RunRevealAnim 自清理;ChooseDisplayIcon 集中 | | **R21** | **95.0** | R20 修复确认;识别 N1(服务重试缺失)N2(readonly)N3(执行顺序) |