# 小地图系统 R26 独立评审报告 **评审轮次**:Round 26(R25 修复后全面重审) **评审基准**:成熟 2D Metroidvania 游戏商业级标准 + 专业编辑器扩展 --- ## 一、各维度评分 | 维度 | 满分 | 得分 | vs R25(修复后) | |------|------|------|----------------| | 架构设计 & 解耦合 | 25 | 24.0 | -0.5 | | 性能 & 运行时效率 | 20 | 19.5 | ±0 | | 编辑器工具质量 | 15 | 14.5 | ±0 | | 代码质量 & 可维护性 | 15 | 13.8 | -0.2 | | 存档 & 持久化 | 10 | 9.5 | ±0 | | 可扩展性 | 10 | 9.5 | ±0 | | 功能完整性 | 5 | 5.0 | ±0 | | **综合** | **100** | **95.8** | **-1.7** | > R25(修复后基准):97.5 → R26:95.8(发现 2 项新问题) --- ## 二、R25 修复全部落地确认 | 修复项 | 状态 | |--------|------| | C1:TeleportService.CanTeleportTo `_mapSvc == null \|\| ...` → `_mapSvc != null && ...` | ✅ | | N1:MapPanel._subs 死代码字段及 Clear() 调用全部移除 | ✅ | | N2:MapInputHandler.Awake 展开 + `#if UNITY_EDITOR \|\| DEVELOPMENT_BUILD` 校验警告 | ✅ | --- ## 三、持续表现优秀的设计 | 维度 | 设计亮点 | |------|---------| | **接口解耦** | IMapService / IPlayerPositionProvider / IPinService / ITeleportService 全通过 ServiceLocator;UI 层无任何具体类引用 | | **执行顺序链** | MapManager(-700)→MapPlayerTracker(-600)→MapPinManager(-500)→TeleportService(-400)→UI(0);服务注册先于所有消费方 | | **空间索引共享** | MapDatabaseSO 统一维护 `_cellToRoom` 字典;MinimapHUD 的 O(viewRadius²) 剔除与 MapPlayerTracker 的 O(1) 房间判定共享同一索引,无重复构建 | | **五类对象池** | MapPanel(Cell/Exit/Pin)+ MinimapHUD(Cell/Pin);全量 Disable 入池、Enable 出池,OnDestroy 销毁剩余;发现动画协程自清理(R20-N1)| | **ISaveable 防御性拷贝** | MapManager / MapPinManager / TeleportService 三处对称:OnSave new HashSet/List,OnLoad Clear+foreach Add | | **_servicesReady 短路** | MapPanel + MinimapHUD 均在三服务就绪后置 true,消除每帧 ServiceLocator 查询 | | **LateUpdate 脏检查** | MapPanel 玩家图标(RoomId + NormPos 双字段);MinimapHUD 玩家圆点(同);Pin 版本号脏检查——全部避免无效 RectTransform 读写 | | **DRY** | ChooseDisplayIcon 集中在 MapRoomDataSO;BuildRegionDict / ResolveRegionDisplayName 集中在 MapServiceExtensions;MapPanel + MinimapHUD 均通过委托调用 | | **编辑器工具** | 拖拽/缩放/验证(4类错误)/搜索(RoomId/RegionId/RoomType三维)/图例/Play Mode 玩家叠加;GUIStyle 缓存;MatchesRoomType 不区分大小写 | | **存档健壮性** | OnLoad 空集合防御、空 id 过滤;OnValidate 自动 Trim RoomId、迁移旧 bool 字段;`delayCall -=;+=` 防止 Inspector 快速操作重复触发 | | **UnsubscribeServices 有意不对称** | MapPanel(OnDestroy 末尾不清空引用)vs MinimapHUD(置 null + 重置标志,支持跨场景重连)——均有注释说明 | --- ## 四、新发现问题 ### N1 — Normal:ITeleportService 接口不完整(`UnlockTeleportStation` / `NotifyTeleportCompleted` 缺失) **文件**:`ITeleportService.cs`(接口缺失),`TeleportService.cs`(L103–115 公开方法) **问题**: ```csharp // ITeleportService 接口中存在的方法: bool CanTeleportTo(string roomId); void RequestTeleport(string targetRoomId); event Action OnTeleportRequested; event Action OnTeleportCompleted; // ─── 缺失的两个写操作方法 ─── // TeleportService 上有 public 定义,但不在接口中: public void NotifyTeleportCompleted(string arrivedRoomId); // 场景加载系统须调用 public void UnlockTeleportStation(string roomId); // 游戏触发器须调用 ``` **影响**: - **场景加载系统**完成传送后需调用 `NotifyTeleportCompleted`,但通过 `ServiceLocator.GetOrDefault()` 只能拿到接口,无法访问该方法;调用方被迫使用具体类型(`TeleportService`)或反射,**破坏接口解耦原则**。 - **传送站触发器**(`OnTriggerEnter` 等)需要调用 `UnlockTeleportStation`,同样面临相同问题。 - 接口 + ServiceLocator 模式在整个系统中一致使用,此处缺口会让后续维护者混淆。 **修复**:在 `ITeleportService` 中补全两个方法: ```csharp /// 解锁指定房间的传送点(游戏触发器调用)。 void UnlockTeleportStation(string roomId); /// 场景加载系统传送完成后调用,触发 OnTeleportCompleted 事件。 void NotifyTeleportCompleted(string arrivedRoomId); ``` --- ### N2 — Normal:MapPinManager 缺少 `_isDuplicate` 单例保护(与 MapManager / MapPlayerTracker 不一致) **文件**:`MapPin.cs`(MapPinManager 类,L32–37) **问题**: ```csharp // MapManager(正确): private void Awake() { if (ServiceLocator.GetOrDefault() != null) { _isDuplicate = true; Destroy(gameObject); return; } ServiceLocator.Register(this); } // MapPlayerTracker(正确): private void Awake() { if (ServiceLocator.GetOrDefault() != null) { _isDuplicate = true; ... } ... } // MapPinManager(缺少保护): private void Awake() { // ← 无重复实例检测 ServiceLocator.Register(this); // 若已有注册,直接覆盖 } ``` **影响**: - 若场景中意外存在两个 MapPinManager(Persistent 场景加载两次、DontDestroyOnLoad 重复等),第二个实例会覆盖 ServiceLocator 注册,但第一个实例的 `ISaveable` 仍保持注册状态(`OnEnable` 中加入 SaveableRegistry),导致存档时 `OnSave` 被调用两次、集合竞争写入 `SaveData.Map.Pins`。 - 系统其他三处服务均有 `_isDuplicate` 守卫,此处缺失属于架构一致性漏洞。 **修复**: ```csharp private void Awake() { if (ServiceLocator.GetOrDefault() != null) { _isDuplicate = true; Destroy(gameObject); return; } ServiceLocator.Register(this); } // OnEnable / OnDisable / OnDestroy 首行加 if (_isDuplicate) return; ``` --- ## 五、各文件评分摘要 | 文件 | 状态 | 备注 | |------|------|------| | MapManager.cs | ✅ 完整 | 执行顺序/ISaveable/IMapService/缓存/LINQ 仅在非热路径 | | MapPlayerTracker.cs | ✅ 完整 | O(1) 空间索引/平滑归一化/单例保护 | | **MapPinManager(MapPin.cs)** | ⚠️ N2 | 缺少 _isDuplicate 保护 | | **TeleportService.cs** | ⚠️ N1 | 接口缺口(UnlockTeleportStation/NotifyTeleportCompleted) | | MapPanel.cs | ✅ 完整 | R25-N1 _subs 死代码已移除;五层解耦;三对象池 | | MinimapHUD.cs | ✅ 完整 | O(viewRadius²) 剔除;跨场景重连;GC 友好缓冲区 | | MapInputHandler.cs | ✅ 完整 | R24-N2 单一 scale 状态;R25-N2 配置警告 | | MinimapInputHandler.cs | ✅ 完整 | 干净委托,单职责 | | MapRoomCellUI.cs | ✅ 完整 | 双场景复用;PlayRevealAnim 颜色恢复 | | MapRoomDataSO.cs / MapDatabaseSO | ✅ 完整 | ChooseDisplayIcon 集中;空间索引共享;OnValidate 含延迟通知防抖 | | MapServiceExtensions.cs | ✅ 完整 | 纯静态无状态;DRY 中心 | | RegionNameDisplay.cs | ✅ 完整 | CompositeDisposable 正确使用;协程 OnDisable 终止安全 | | **ITeleportService.cs** | ⚠️ N1 | 缺少两个写操作方法声明 | | MapLayoutEditorWindow.cs(编辑器) | ✅ 完整 | 搜索+图例+拖拽+验证+GUIStyle 缓存+✕按钮 | --- ## 六、修复优先级 | 编号 | 严重度 | 文件 | 影响 | 建议 | |------|--------|------|------|------| | N1 | 🟡 Normal | ITeleportService.cs + TeleportService.cs | 接口不完整导致调用方被迫使用具体类型 | 立即修复 | | N2 | 🟡 Normal | MapPin.cs(MapPinManager) | 多实例场景存档竞争写入 | 立即修复 | --- ## 七、评分历史 | 轮次 | 综合评分 | 主要变化 | |------|---------|---------| | R17 | 93.0 | 初轮重构后 | | R18–R19 | 93.8 | 平移/缩放/协程修复 | | R20 | 94.2 | DRY + 协程自清理 | | R21 | 95.0 | ServicesReady 对称 | | R22 | 96.5 | BuildRegionDict 集中 | | R23 | 97.5 | MatchesRoomType + 执行顺序 | | R24 | 97.5 | TeleportService 重建(遗留 C1) | | R25(修复前) | 95.5 | 发现 C1 逻辑反转 + N1 死代码 + N2 无警告 | | R25(修复后基准) | 97.5 | 三项全修复 | | **R26** | **95.8** | 发现 N1(接口不完整-1.2)+ N2(单例缺失-0.5)| --- *R26 评审完成时间:2026-05-25*