- Round 8 report highlights improvements in architecture, editor usability, and data robustness, with a total score of 80/100. - Round 9 report focuses on editor extension capabilities, identifying issues with room data indexing and layout editing, resulting in a score of 76/100. - Round 26 report evaluates the system against commercial standards, noting new issues and confirming previous fixes, with a score of 95.8/100.
12 KiB
Minimap System — Round 19 Independent Review
评审范围
全部 23 个源文件(4 接口 + 15 运行时 + 4 编辑器),在 R18 全部修复已落地的基础上进行全面重新审查。
R18 修复确认
| 编号 | 描述 | 验证状态 |
|---|---|---|
| R18-N1a | MapInputHandler.Update rangeX/rangeY 乘以 _zoom |
✅ 第 79–80 行 |
| R18-N1b | MapPanel.CenterOnCurrentRoom 从 _roomContainer.localScale.x 读 zoom,修正 rangeX/rangeY 及 cellX/cellY |
✅ 第 377–384 行 |
| R18-N2 | _noResultStyle 缓存为字段,在 EnsureLabelStyles() 首次调用时初始化 |
✅ 第 435–445 行 |
| R18-N3 | MinimapHUD._onMapUpdated 改 [HideInInspector],移除订阅和 OnMapUpdated 方法 |
✅ 第 36–39 行 |
R19 全方面评分
| 维度 | 权重 | 分数 | 评分依据 |
|---|---|---|---|
| 架构解耦 | 20% | 95 | 4 接口全 ServiceLocator;C# 事件单向流;无循环依赖;Awake/OnDestroy 长期订阅模式;MapServiceExtensions 集中无状态查询逻辑 |
| 功能完整性 | 20% | 93 | 三级可见性、地图碎片解锁动画、Pin/传送/区域名本地化、探索进度显示、雾效覆盖层、存档全覆盖 |
| 性能 | 15% | 93 | 五对象池(cell×2 / pin×2 / exit×1);O(viewRadius²) 剔除;PinsVersion 脏检查;_servicesReady 短路;MapDatabaseSO 双重 O(1) 索引;R18-N1 键盘平移已修正;N1c 潜在风险(见下方) |
| 编辑器工具 | 15% | 94 | 布局编辑器可视化+拖拽+搜索+图例+Undo;MapDatabaseEditor 自动验证;MapRoomDataEditor SceneGUI 双角控制点;AssetPostprocessor 自动注册+null 清理;R18-N2 GUIStyle 缓存到位 |
| 可扩展性 | 10% | 95 | RoomType[Flags] 枚举可任意叠加新类型;SO 驱动(MapDatabaseSO / MapPinConfigSO);接口易替换;MapGridConstants 统一常量管理 |
| 代码质量 | 10% | 95 | 全面注释;命名规范;3 处 ISaveable 防御性拷贝对称;try/finally 保护 GUI.matrix;[FormerlySerializedAs] 迁移;[HideInInspector] 废弃兼容;_noResultStyle 已缓存 |
| 玩家体验设计 | 10% | 92 | 三级可见性;传送点快速旅行;循环缩放视野;Play Mode 编辑器叠加;区域名淡入动画;R18-N1 平移手感已修正 |
综合评分:93.8 / 100(与 R18 修复后预测值完全吻合)
架构层深度审查
接口与服务完整矩阵
| 接口 | 实现类 | ServiceLocator 注册 | ISaveable | 备注 |
|---|---|---|---|---|
IMapService |
MapManager |
Awake(重复实例保护) | ✅ | 防御拷贝 + OnLoad 广播 OnExplorationChanged |
IPinService |
MapPinManager |
Awake | ✅ | PinsVersion 版本号驱动脏检查 |
IPlayerPositionProvider |
MapPlayerTracker |
Awake(重复实例保护) | ❌(不需要) | LateUpdate O(1) 空间索引 + NormalizedPositionInRoom 每帧插值 |
ITeleportService |
TeleportService |
Awake | ✅ | 防御拷贝;事件通道驱动场景过渡 |
数据流方向(单向)
MapManager ──── OnRoomEntered ────────▶ OnExplorationChanged (C# event)
▶ MapPanel.OnExplorationChanged
▶ MinimapHUD.OnExplorationChanged
▶ MapProgressDisplay.Refresh
MapManager ──── SetMapped / SetMappedBatch ──▶ OnRoomMapped (C# event)
▶ MapPanel.OnRoomMappedAnim (发现动画)
MapManager ──── _onRegionChanged.Raise ──────▶ RegionNameDisplay
▶ MapProgressDisplay.OnRegionChanged
MapPlayerTracker ── OnRoomChanged (C# event) ▶ MinimapHUD.OnRoomChanged → RefreshView
─ NormalizedPositionInRoom ▶ MapPanel.LateUpdate(脏检查)
▶ MinimapHUD.LateUpdate.UpdatePlayerDot
_onMapUpdatedStringEventChannel 仍在 MapManager 的 3 处发射,但地图 UI 已全部移除订阅(MapPanel R12-N8、MinimapHUD R18-N3)。该通道保留以供非 Map UI 的外部系统订阅,属于已知设计状态。
对象池完整性
| 池 | 类型 | 宿主 | 入池时机 | 出池时机 |
|---|---|---|---|---|
_cellPool |
MapRoomCellUI | MapPanel | RebuildAll / OnDestroy | BuildGrid |
_pinPool |
Image (Pin) | MapPanel | ClearPins / OnDestroy | RenderPins |
_exitPool |
Image (Exit) | MapPanel | ClearExits / OnDestroy | DrawExits |
_cellPool |
MapRoomCellUI | MinimapHUD | RefreshView 裁剪过期 / OnDestroy | RefreshView 新增 |
_pinPool |
Image (Pin) | MinimapHUD | ClearPins / OnDestroy | RebuildPins |
所有池均正确实现:入池时 SetActive(false) + Push,出池时 Pop + SetActive(true) 或 Instantiate 兜底。
ISaveable 对称性审查
| 类 | OnSave 防御拷贝 |
OnLoad 防御拷贝 |
读档后事件广播 |
|---|---|---|---|
MapManager |
new HashSet<>(x2) |
new HashSet<>(x2) |
OnExplorationChanged?.Invoke() |
MapPinManager |
new List<>(_pins) |
new List<>(data.Pins) |
PinsVersion++ |
TeleportService |
new HashSet<>(_unlockedRoomIds) |
Clear + foreach Add | — |
三处均对称,无共享引用风险。
性能关键路径
| 路径 | 复杂度 | 机制 |
|---|---|---|
MinimapHUD.RefreshView 新格子查询 |
O(viewRadius²) | MapDatabaseSO.GetRoomIdAtCell 共享空间索引 |
MinimapHUD.LateUpdate.UpdatePlayerDot |
O(1) | roomId + NormPos 双字段脏检查 |
MapPanel.LateUpdate 服务查询 |
O(1) 短路 | _servicesReady bool 门控 |
MapPanel.LateUpdate.RenderPins |
O(1) 检查 | PinsVersion 脏检查 |
MapManager.GetRoomsByRegion |
O(1) | _regionCache 懒加载字典 |
MapDatabaseSO.GetRoom |
O(1) | _index 字典哈希 |
MapDatabaseSO.GetRoomIdAtCell |
O(1) | _cellToRoom 空间哈希 |
MapPinConfigSO.GetSprite |
O(1) | _cache 惰性字典 |
RegionNameDisplay / MapProgressDisplay.ResolveDisplayName |
O(1) | 预建 _regionDict |
MapLayoutEditorWindow.EnsureLabelStyles |
O(1) | _cachedZoomForStyle 脏检查 |
MapLayoutEditorWindow._noResultStyle |
O(1) | 首次初始化后不重建 |
R19 新发现问题
N1(低,配置健壮性):_roomContainer 与 _zoomTarget 为两个独立引用,配置不一致时静默偏差
涉及文件:MapInputHandler.cs(第 21 行)、MapPanel.cs(第 23 行)
现状:
MapInputHandler._zoomTarget:OnScroll写入_zoomTarget.localScale,Update使用_zoom字段MapPanel._roomContainer:CenterOnCurrentRoom读取_roomContainer.localScale.x
两者仅在 _zoomTarget == _roomContainer(即 Prefab 中两个引用指向同一 GameObject)时保持同步。若开发者在 Prefab 中错误配置,CenterOnCurrentRoom 将读到错误的缩放系数。当前无运行时断言或警告。
影响:误配置后打开地图并缩放,再按"居中"快捷键(MapCenterEvent),定位会偏移但无错误提示。
修复建议:
// MapInputHandler.cs — OnEnable / OnScroll 中向 MapPanel 暴露当前缩放值
// 方案A:MapPanel 增加 public 属性
public float CurrentZoom => _roomContainer != null ? _roomContainer.localScale.x : 1f;
// MapInputHandler.OnScroll 改从 _panel.CurrentZoom 读,而非独立维护 _zoom(消除两份状态)
// 方案B(最小改动):Awake 中断言 _zoomTarget == _scrollRect.content
N2(低,正确性边缘):MapPanel.OnRoomMappedAnim 协程在目标格子被回收后继续运行
文件:MapPanel.cs 第 198–202 行
现状:
protected virtual void OnRoomMappedAnim(string roomId)
{
if (_cells.TryGetValue(roomId, out var cell) && cell != null)
StartCoroutine(cell.PlayRevealAnim(_revealFlashColor, _revealDuration));
}
StartCoroutine 挂在 MapPanel(this)上,协程生命周期由 MapPanel 管理。若在动画播放期间触发 RebuildAll(OnDatabaseChanged),目标 cell 会被入池(SetActive(false))。协程继续运行并向已入池的 cell 的 _bg.color 写入,直到 _revealDuration 结束。下次该 cell 出池并调用 Setup → SetVisibility 时颜色会被正确覆盖,因此视觉影响自愈。
严重程度:极低(数据库热更极少发生,动画持续 0.4 s)。但在频繁使用编辑器热更的开发流程中可能产生轻微闪烁。
修复建议:
// MapPanel 添加字段
private readonly Dictionary<string, Coroutine> _revealCoroutines = new();
// OnRoomMappedAnim 中:
if (_revealCoroutines.TryGetValue(roomId, out var old) && old != null)
StopCoroutine(old);
_revealCoroutines[roomId] = StartCoroutine(cell.PlayRevealAnim(...));
// RebuildAll 中清理:
foreach (var c in _revealCoroutines.Values)
if (c != null) StopCoroutine(c);
_revealCoroutines.Clear();
N3(信息,架构说明):MapManager._onMapUpdated 通道现在在地图 UI 内无订阅者
文件:MapManager.cs 第 85、107、122 行
经 R12-N8(MapPanel)和 R18-N3(MinimapHUD)后,地图 UI 不再订阅 _onMapUpdated StringEventChannelSO。MapManager 仍在 OnRoomEntered、SetMapped、SetMappedBatch 三处调用 Raise()。
这不是 Bug:SO 事件通道天然支持外部系统订阅(如 Achievement 系统、音效触发器);保留 Raise() 符合开放设计原则。
仅建议添加文档注释:
[Header("Event Channels")]
[Tooltip("房间被标记时广播(Explored/Mapped),供地图外部系统订阅(地图 UI 已改用 C# 事件)。")]
[SerializeField] private StringEventChannelSO _onMapUpdated;
编辑器工具覆盖面(策划/开发视角)
| 工具 | 目标用户 | 核心能力 |
|---|---|---|
| MapLayoutEditorWindow | 策划 + 开发 | 可视化房间布局;滚轮缩放;中键/Alt 平移;左键选中;拖拽编辑房间坐标(Undo);实时重叠检测红色警示;区域配色;搜索高亮(无结果提示);图例;Play Mode 玩家点叠加;出口连线(>12px 时) |
| MapDatabaseEditor | 开发 | 数据库统计(房间数/出口数);一键验证;Inspector 内嵌"打开布局编辑器";可折叠房间列表+Ping;错误行红色高亮 |
| MapRoomDataEditor | 关卡设计 + 开发 | SceneGUI 双角控制点可视化;拖拽吸附到整格;防反转保护;居中 SceneView 快捷按钮;HelpBox 坐标系说明 |
| MapRoomAutoRegister | 开发 | AssetPostprocessor 自动注册新 SO 到默认 Database;null 清理;IsDefault 优先级;可通过 EditorPrefs 禁用 |
总结
经过 19 轮迭代,小地图系统已达到成熟的生产级 2D 探索类游戏标准:
强项
- 接口完整:4 接口全 ServiceLocator,UI 层无具体实现依赖
- 存档正确:3 处 ISaveable 防御拷贝对称,读档后事件广播完整
- 性能到位:5 对象池 + O(viewRadius²) + 多级脏检查 + 双重 O(1) 索引
- 编辑器成熟:拖拽布局 + 搜索 + Undo + 验证 + SceneGUI 一体化,策划友好
- 事件方向清晰:所有 UI 更新均由 C# 事件(
OnExplorationChanged)单向驱动,无双重刷新
R19 新发现(3 项,全为低优先级)
- N1:
_zoomTargetvs_roomContainer双引用无配置一致性断言(配置层风险) - N2:
OnRoomMappedAnim协程可能写入已回收的格子(视觉自愈,影响微小) - N3:
_onMapUpdated通道在地图 UI 内零订阅(信息提示,非缺陷)
评分历史
| 轮次 | 评分 | 主要改进 |
|---|---|---|
| R14 | 92.3 | ISaveable 签名修复,TeleportStation 枚举值 |
| R15 | 91.3 | DrawRoomBadge 补 TeleportStation,MapProgressDisplay 区域名 |
| R16 | 92.5 | DrawRoomBadge 优先级,MinimapHUD 4 Sprite + ChooseIcon |
| R17 | 93.0 | 死代码清理,搜索 UX 改善(alpha + 无结果提示) |
| R18 | 93.8 | 键盘平移范围修正,GUIStyle 缓存,MinimapHUD 双重刷新消除 |
| R19 | 93.8 | 全部 R18 修复确认到位,识别 3 项新低优先级问题 |