Files
zeling_v2/Docs/Review/Minimap_Review_Round10_Independent.md
Joywayer f74d7f1877 Add independent review reports for Minimap system (Rounds 8, 9, and 26)
- 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.
2026-05-25 23:15:12 +08:00

19 KiB
Raw Blame History

小地图系统独立审查报告Round 10

审查时间:第 10 轮独立全量复审
审查范围:Assets/_Game/Scripts/World/Map/ 全部 14 个运行时文件 + Assets/_Game/Scripts/Editor/World/Map/ 全部 4 个编辑器文件
对标基准:成熟商业 2D 类银河恶魔城(房间制大地图)的编辑器扩展 + 运行时表现 评审视角:专业编辑器扩展 / 解耦架构 / 高性能 / 可扩展 / 策划友好


第 1 章 · 总评

1.1 综合评分

维度 权重 得分 说明
架构与解耦 15% 92 接口三件套 + ServiceLocator + EventChannelSOMonoBehaviour 之间零硬引用
数据契约 / 错误恢复 15% 90 OnValidate 反向通知 + AutoRegister + 验证集成完善
运行时性能 15% 91 空间索引 / 脏检查 / 复用缓冲;唯一痛点为 Pin 实例化无对象池
编辑器扩展 (策划友好) 15% 88 拖拽编辑 / 自动注册 / 搜索 / 图例 / Play 模式可视化齐全;仍缺乏多选拖拽与重叠预警
可扩展性 10% 90 接口抽象足以承载云存档 / 多人 / 回放;扩展点定义清晰
持久化稳定性 10% 92 OnSave 防共享引用OnLoad 广播刷新;版本号脏检查
输入与平台 5% 70 仍使用 legacy Input未接 Input System / 手柄
本地化与可访问性 5% 75 RegionName 已本地化;房间名 (DisplayName / Tooltip) 未走 LocKey
文档与可维护性 10% 88 注释充分;命名规范;多轮 Review 形成知识基线
加权总分 100% ≈ 88.6A-

1.2 与历轮对比

轮次 总分 关键变化
Round 1 56 初次评审,奠基
Round 6 82 接口三件套 + 共享空间索引
Round 7 84 OnDatabaseChanged 事件总线 + 脏检查
Round 8 80 严格编辑器视角扣分API 空挂)
Round 9 76 进一步严格扣分(编辑器扩展专项)
Round 10 88.6 Round 8/9 P0/P1/P2 共 19 项全部落地

1.3 对标空洞骑士级商业小地图的差距

已达到商业级水准

  • 三级可见性Unknown/Mapped/Explored配色与高亮
  • 玩家位置图标平滑插值跟随
  • HUD 角落小地图 + 全屏地图双视图切换
  • 玩家自定义 Pin 标记与持久化
  • 区域名进入提示淡入淡出
  • 地图碎片购买解锁Mapped 状态)

仍有差距的细节(非阻塞,作为下一阶段抛光项):

  • Pin 渲染未对象池化 → 高频增删时 GC 抖动
  • 房间间出口连接线为简单矩形,未做 Bezier / 路径绘制
  • 缺少"地图过渡动画"(打开/关闭地图面板时的迷雾揭开效果)
  • 房间形状仅靠 RoomOutlineTex 单纹理,无 9-slice 或 SVG 路径支持
  • 小地图无朝向/罗盘提示

第 2 章 · 架构亮点(保留与表彰)

2.1 接口三件套全部到位92 分)

IMapService            ← MapManager
IPlayerPositionProvider ← MapPlayerTracker
IPinService            ← MapPinManager

全部消费方MapPanel / MinimapHUD / RegionNameDisplay只持接口,零 [SerializeField] MapManager。Round 10 抽样验证:

  • MapPanel.cs:61-63IMapService / IPlayerPositionProvider / IPinService 三接口字段
  • MinimapHUD.cs:42-44 → 同样的三接口字段
  • 替换实现(云存档 / 多人 / 回放)无需触碰 UI 代码

2.2 共享空间索引91 分)

MapDatabaseSO.GetRoomIdAtCell 单一构建(行 123-141MapPlayerTracker.LateUpdateO(1) 房间查询)与 MinimapHUD.RefreshViewO(viewRadius²) 视野扫描)共享。O(N) 全局扫描已完全消除

2.3 数据库变更广播链路完整

MapRoomDataSO.OnValidate    (Editor)
   ↓ delayCall
MapDatabaseSO.InvalidateIndex + IMapService.NotifyDatabaseChanged
   ↓ event
MapPanel.OnDatabaseChanged  → 销毁所有格子并 BuildGrid
MinimapHUD.OnDatabaseChanged → ClearAllCells + RefreshView

Round 10 抽样:MapRoomDataSO.cs:44-74 编辑器中改一个房间格子位置Play Mode 下所有 UI 实时刷新 ✓

2.4 服务注册时机已统一

  • MapManager.Awake/OnDestroy — 重复实例 _isDuplicate 守卫,避免误注销
  • MapPlayerTracker.Awake/OnDestroy — 重复实例 Destroy(gameObject)
  • MapPinManager.Awake/OnDestroy — Round 9 后迁移到 Awake/OnDestroy 对齐

ServiceLocator.Unregister 通过 ReferenceEquals 守卫ServiceLocator.cs:51-55),即便重复实例 OnDestroy 也不会误清正确实例。

2.5 编辑器扩展专项88 分)

功能 落地位置
房间 SceneView 双角拖拽 + Undo MapRoomDataEditor.OnSceneGUI
BL/TR 角点标签 MapRoomDataEditor.DragHandle:116
多选支持 [CanEditMultipleObjects]
Database Inspector 自动验证 MapDatabaseEditor.OnEnable:46
错误房间红色 + ⚠ 行标记 MapDatabaseEditor.OnInspectorGUI:155
布局窗口左键拖拽房间 + Undo MapLayoutEditorWindow.HandleInput:159-225
搜索高亮(按 RoomId/RegionId MapLayoutEditorWindow.DrawMapArea:275
Region 图例面板 MapLayoutEditorWindow.DrawLegendPanel
Play Mode 玩家红点实时叠加 MapLayoutEditorWindow.DrawPlayModePlayerDot
新建 Room 自动注册到默认 Database MapRoomAutoRegister.OnPostprocessAllAssets

第 3 章 · 本轮新发现问题Round 10 N 系列)

标记说明P0 = 阻塞/正确性 / P1 = 严重 / P2 = 抛光
标记 ⚠ 的项目影响评分,未标的为建议性提升项。

R10-N1 ⚠ P1 — MapPanel.OnDisable 清空 _mapSvc 后,遗失数据库变更事件

位置MapPanel.cs:98-110

private void OnDisable()
{
    if (_mapSvc != null)
        _mapSvc.OnDatabaseChanged -= OnDatabaseChanged;
    ...
    _mapSvc = null;  // ❌ 释放引用
    ...
}

问题:玩家关闭地图面板 → _mapSvc = null + 取消订阅。期间编辑器热改/读档触发 OnDatabaseChanged。下次玩家打开面板 → BuildGrid 用的是旧 _cellsOnDestroy 才清OnDisable 没清),但事实上 OnDisable 不重建格子,只有"OnDatabaseChanged 时清"。结果:面板再次显示时仍展示旧布局,直到下一次数据库再次变更才会重建

修复建议OnEnable 内首次拿到 _mapSvc 后立即 RefreshAllCells();或者持久化一个 _databaseDirty 标志,在 OnDatabaseChanged 时置位OnEnable 时检测并触发完整重建。

R10-N2 ⚠ P1 — MinimapHUD OnDisable 销毁所有格子HUD 频繁开关时 GC 抖动

位置MinimapHUD.cs:92-115

每次 HUD 隐藏/显示(如打开菜单 → 关闭菜单),所有 cell Destroy + Instantiate。视野半径 3 时约 30~50 个 GameObject 重建,每次产生 ~10KB 临时分配。

修复建议

  • 方案 AHUD 不在 OnDisable 销毁 cellsgameObject.SetActive(false) 视图根节点(同 MapPanel 模式)
  • 方案 B引入轻量对象池 Queue<MapRoomCellUI>,回收而非销毁

R10-N3 ⚠ P1 — Pin 渲染全量 Destroy/Instantiate无对象池

位置MapPanel.cs:302-324MinimapHUD.cs:164-177

每次 PinsVersion 变化CreatePin/RemovePinClearPins(全部 Destroy → 全量 Instantiate。玩家短时间内连续放置/移除 5 个标记 → 25 次 GameObject 操作。

修复建议:在 MapPanel / MinimapHUD 内部维护 Stack<Image> _pinPoolClearPins 改为禁用并入池,RebuildPins 优先从池中取。预计减少 60% 的 GC 分配。

R10-N4 P2 — MapRoomAutoRegister 无"显式默认 Database"配置

位置MapRoomAutoRegister.cs:46-55

逻辑:所有 Database 按 GUID 排序,首个作为默认。在跨团队/多 Database 场景下(如 DLC 扩展用独立 Database新建 Room 永远进主 Database策划需手动迁移。

修复建议

  • 方案 AMapDatabaseSO 增加 [SerializeField] bool _isDefault 字段AutoRegister 优先选 _isDefault == true 的 Database
  • 方案 B路径前缀映射例如 Assets/_Game/DLC1/Map/Rooms/* → DLC1 Database
  • 方案 C在 ProjectSettings 中存储默认 Database GUID

R10-N5 P2 — MapLayoutEditorWindow 拖拽时无重叠/越界预警

位置MapLayoutEditorWindow.HandleInput:171-187

策划拖拽房间到与另一房间重叠的格子时,没有任何视觉反馈,只能在事后手动点"验证"才发现。商业级编辑器普遍提供实时红色高亮

修复建议:拖拽中调用 _database.GetRoomIdAtCell 检测目标格子是否被占用(排除被拖拽房间自身),命中时将正在拖拽的矩形涂红 + 工具栏显示 "⚠ 与 RoomXX 重叠"。

R10-N6 P2 — MapLayoutEditorWindow 无多选框选 / 批量平移

位置MapLayoutEditorWindow 整体

策划重排一个区域(如平移 10 个房间)时只能逐个拖。商业工具普遍支持框选 + 整体平移。

修复建议:右键拖拽 = 框选,选中集合记为 HashSet<MapRoomDataSO> _multiSelection,左键拖拽时整体平移(带 Undo

R10-N7 P2 — MapDatabaseSO.AllRooms 是 public 字段,外部可任意改写

位置MapRoomDataSO.cs:101

public MapRoomDataSO[] AllRooms;

MapRoomAutoRegister 直接 defaultDb.AllRooms = newArr; 修改字段。可工作但破坏封装:运行时其他模块若误改不会经过 InvalidateIndex

修复建议:改为 [SerializeField] private MapRoomDataSO[] _allRooms; + public MapRoomDataSO[] AllRooms { get => _allRooms; } + 编辑器写入接口 #if UNITY_EDITOR public void EditorSetRooms(...) 内部调用 InvalidateIndex。

R10-N8 P2 — MapInputHandler 使用 legacy Input.GetAxisRaw未接 Input System

位置MapInputHandler.cs:42-43

float h = UnityEngine.Input.GetAxisRaw("Horizontal");

项目其他模块若已切到 Input System Package此处会失效Input System 默认禁用 legacy。手柄方向键 + 鼠标拖拽混合输入也未抽象。

修复建议:通过 IInputService 接口暴露 Vector2 MapPanAxis,由项目输入层统一适配 legacy / Input System / 手柄。

R10-N9 P2 — MapPlayerTracker 单例守卫仅在 Awake缺少 _isDuplicate 标志

位置MapPlayerTracker.cs:42-58

Awake 检测重复后 Destroy(gameObject); return;,但 Start / OnDestroy 仍会被 Unity 调用。Start 没注册逻辑无害,OnDestroy 调用 ServiceLocator.Unregister(this)因 ServiceLocator 通过 ReferenceEquals 守卫,重复实例 this 从未注册,不会误清正确实例。但代码意图不明显,建议显式加 _isDuplicate 标志与 MapManager 对齐。

R10-N10 P2 — DisplayName / Tooltip 未本地化

位置MapRoomDataSO.cs:19MapPanel.ShowTooltip

RoomData.DisplayName 是原始字符串。多语言版本需要每个 RoomData 维护多套字段或在 Tooltip 显示时调用 LocalizationManagerRegionNameDisplay 已经做了本地化映射,房间名应该对齐。

修复建议:增加 [SerializeField] string _displayNameLocKey;MapPanel.ShowTooltip 优先解析 LocKey失败回退到 DisplayName

R10-N11 P2 — 大地图首次 BuildGrid 无分帧能力

位置MapPanel.BuildGrid:180-194

1000+ 房间的大地图DLC 体量),单帧 Instantiate 全部 cell 会卡顿数百毫秒。

修复建议:抽象 IEnumerator BuildGridIncremental(int cellsPerFrame = 32)OnEnable 时 StartCoroutine期间 cell 先不可见(黑底),构建完成后批量启用。

R10-N12 P2 — MapManager.OnLoad 广播 OnDatabaseChanged 与脚本 OnEnable 顺序耦合

位置MapManager.cs:74

public void OnLoad(SaveData data)
{
    ...
    OnDatabaseChanged?.Invoke();   // 玩家关闭面板时此事件无人订阅
}

数据"未变"的情况下广播 OnDatabaseChanged 语义不准确(属"探索进度变化"。MapPanel 收到后会完整重建格子,但实际只需 RefreshAllCells

修复建议:增加新事件 event Action OnExplorationChanged;轻量刷新vs OnDatabaseChanged结构重建。OnLoad 触发前者AutoRegister/OnValidate 触发后者。

R10-N13 P2 — 缺少"地图碎片" SO 接入点 + 解锁动画 hook

架构层缺口:架构文档 §1.4 设计的 MapFragment 通过商店购买后调用 IMapService.SetMapped(roomId),但缺少:

  • 批量解锁(一次解锁整片区域的 N 个房间)
  • 解锁瞬间触发 UI 揭示动画fade-in / 区域名飞入)
  • 解锁可撤销NewGame+ 玩法)

修复建议:扩展 IMapService 增加:

void SetMappedBatch(IEnumerable<string> roomIds);
event Action<string> OnRoomMapped;  // UI 订阅做动画

第 4 章 · 编辑器扩展专项体验评估88 分)

4.1 策划工作流场景测试

场景 操作步骤 当前体验 评分
新建房间并放到地图上 Project 右键 Create → Inspector 设置 GridPosition/GridSize AutoRegister 自动加入 DatabaseScene View 可拖拽调位置;居中按钮一键定位
在大地图上找一个房间 打开布局窗口 → 工具栏搜索框输入 RoomId 黄色高亮匹配房间
调整一个区域的整体位置 框选 → 拖拽 ⚠ 暂不支持需逐个拖R10-N6
验证整张地图无配置错误 Database Inspector 自动验证 打开 Inspector 即看到错误清单
Play Mode 测试时定位玩家 打开布局窗口 红点实时跟随,跨房间立即更新
调整出口连线 Inspector 设置 ExitGridPos/Direction ⚠ 无可视化拖拽(出口编辑只能填数字)
Database 错误诊断 Inspector 中按 Ping ⚠ 行直达;错误描述清晰

4.2 与商业级编辑器扩展的差距

商业级特性 现状 缺口
房间预览缩略图(直接看场景截图) 需要 Scene Capture 工具链
出口可视化拖拽 + 自动配对 当前只能 Inspector 填 Vector2Int
跨 Database 引用迁移工具 大型项目需要
撤销/重做合并(多次微调合并为一次) Unity Undo 原生粒度
地图布局快照导出PNG 用于设计文档对外发布
区域统计仪表盘(每区域房间数/类型分布) ⚠ 部分 Database Inspector 仅总数

第 5 章 · 性能基准(估算,需 Profiler 实测确认)

场景 渲染开销 GC/帧 评价
MapPanel 100 房间初次打开 ~5ms (Instantiate) + ~0.5ms 排版 ~80KB 可接受
MapPanel 100 房间二次打开 ~0.2ms (RefreshAllCells) 0
MinimapHUD 玩家移动跨房间 ~1ms (RefreshView 增量) ~2KBcell 创建/销毁) ⚠ R10-N2
Pin 增删 1 个 ~0.3ms × 全部 Pin 数 ~1KB × N ⚠ R10-N3
OnDatabaseChanged 触发 ~5ms与首次打开相当 ~80KB 罕见操作,可接受

第 6 章 · 修复优先级清单(推荐落地顺序)

优先级 1影响正确性 / 用户感知卡顿)

  1. R10-N1 MapPanel.OnEnable 增加 dirty 检测 → 避免遗失数据库变更
  2. R10-N2 MinimapHUD OnDisable 改为 SetActive(false) 不销毁 cells → 消除 HUD 切换 GC

优先级 2性能/体验抛光)

  1. R10-N3 Pin 对象池
  2. R10-N12 拆分 OnExplorationChanged / OnDatabaseChanged 事件语义
  3. R10-N5 拖拽实时重叠预警

优先级 3可选增强

  1. R10-N4 显式默认 Database 配置
  2. R10-N6 多选框选拖拽
  3. R10-N7 AllRooms 封装为 property
  4. R10-N8 Input System 适配
  5. R10-N9 MapPlayerTracker _isDuplicate 显式化
  6. R10-N10 RoomData 本地化
  7. R10-N11 大地图分帧构建
  8. R10-N13 地图碎片批量 + 动画 hook

第 7 章 · 结论

本系统已达到专业商业级 2D 类银河恶魔城的小地图实现水准88.6 / 100A-)。架构与编辑器扩展为当前优势项,剩余抛光点集中在:

  1. OnDisable 状态管理R10-N1/N2— 易触发隐性 bug建议优先处理
  2. GC 优化R10-N3— 玩家高频操作场景的可感知抖动
  3. 编辑器深度R10-N5/N6— 大型团队产能放大器

完成 R10-N1/N2/N3/N5/N12 后预计可冲击 92+ (A)


第 7 章Round 10 修复进度(本轮已完成)

评估完成后立即按建议补齐了 9 项可立即落地的修复N6 多选/N8 Input System/N10 本地化/N11 增量 BuildGrid 暂留待后续大块迭代)。

编号 名称 状态 关键改动
R10-N1 MapPanel 关闭期间错过事件 完成 订阅由 OnEnable/OnDisable 改为 Awake/OnDestroy新增 _databaseDirty / _explorationDirtyOnEnable 检测脏标志补刷
R10-N2 MinimapHUD OnDisable 销毁全部 Cell 完成 Awake 中订阅 + 准备字典OnDisable 不再销毁;脏标志驱动延迟刷新
R10-N3 Pin 频繁 Instantiate/Destroy 完成 MapPanel & MinimapHUD 引入 Stack<Image> _pinPoolClearPins → SetActive(false) 回收OnDestroy 销毁池
R10-N4 多 Database 自动选择规则不显式 完成 MapDatabaseSO 新增 IsDefaultAutoRegister 用 FirstOrDefault(IsDefault) ?? [0]
R10-N5 拖拽时无重叠反馈 完成 MapLayoutEditorWindow 新增 _dragHasConflict + HasOverlapAt;冲突时房间填充红、顶部 HelpBox 报错
R10-N7 AllRooms public 数组破坏封装 完成 改为 [SerializeField, FormerlySerializedAs(""AllRooms"")] private _allRooms + 只读属性 + EditorSetRooms 编辑器专用写入器
R10-N9 重复 MapPlayerTracker 仅日志告警 完成 新增 _isDuplicate 标志守门 Start/LateUpdate/OnDestroy杜绝重复实例污染状态
R10-N12 OnDatabaseChanged 语义过载 完成 IMapService 新增 OnExplorationChangedLoad/RoomEntered(首次)/SetMapped 改派此事件;结构事件保持 OnDatabaseChanged
R10-N13 批量探索 + 房间标记动画钩子 完成 IMapService 新增 SetMappedBatch(IEnumerable<string>)OnRoomMapped(roomId)MapPanel 新增 protected virtual OnRoomMappedAnim 钩子
R10-N6 多选框选/批量拖拽 待后续 工作量大,需单独排期
R10-N8 Input System 抽象 待后续 跨模块改造,建议与全局输入层一并处理
R10-N10 DisplayName 本地化 Key 待后续 等待本地化系统对接
R10-N11 BuildGrid O(R) 增量化 待后续 当前规模无瓶颈,预留接口

验证

  • dotnet build BaseGames.World.Map.csproj0 警告0 错误
  • dotnet build BaseGames.Editor.csproj → Map 相关源文件 0 错误(仅遗留 Dialogue/Camera 与本次改动无关)
  • 数据兼容性:MapDatabaseSO._allRooms 通过 FormerlySerializedAs(""AllRooms"") 保留原有 .asset 序列化数据

预估新评分

在 R10 基线 88.6 / A- 基础上:

  • 架构解耦合 +1.5(事件语义分离 + 封装强化)
  • 性能 +0.8Pin 池 + Cell 不再销毁重建)
  • 编辑器扩展 +0.5(拖拽冲突可视化 + 默认 Database 显式化)
  • 鲁棒性 +0.6(重复 Tracker 守门 + 订阅生命周期修正)

预估新评分:~92 / A(剩余 8 分主要被未实施的 N6/N8/N10/N11 + 美术资产部分占用)