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

371 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 小地图系统独立审查报告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-63``IMapService / IPlayerPositionProvider / IPinService` 三接口字段
- `MinimapHUD.cs:42-44` → 同样的三接口字段
- 替换实现(云存档 / 多人 / 回放)无需触碰 UI 代码
### 2.2 共享空间索引91 分)
`MapDatabaseSO.GetRoomIdAtCell` 单一构建(行 123-141`MapPlayerTracker.LateUpdate`O(1) 房间查询)与 `MinimapHUD.RefreshView`O(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`
```csharp
private void OnDisable()
{
if (_mapSvc != null)
_mapSvc.OnDatabaseChanged -= OnDatabaseChanged;
...
_mapSvc = null; // ❌ 释放引用
...
}
```
**问题**:玩家关闭地图面板 → `_mapSvc = null` + 取消订阅。期间编辑器热改/读档触发 `OnDatabaseChanged`。下次玩家打开面板 → `BuildGrid` 用的是旧 `_cells`OnDestroy 才清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 销毁 cells`gameObject.SetActive(false)` 视图根节点(同 MapPanel 模式)
- 方案 B引入轻量对象池 `Queue<MapRoomCellUI>`,回收而非销毁
### R10-N3 ⚠ P1 — Pin 渲染全量 Destroy/Instantiate无对象池
**位置**`MapPanel.cs:302-324``MinimapHUD.cs:164-177`
每次 `PinsVersion` 变化CreatePin/RemovePin`ClearPins`(全部 Destroy → 全量 Instantiate。玩家短时间内连续放置/移除 5 个标记 → 25 次 GameObject 操作。
**修复建议**:在 `MapPanel` / `MinimapHUD` 内部维护 `Stack<Image> _pinPool``ClearPins` 改为禁用并入池,`RebuildPins` 优先从池中取。预计减少 60% 的 GC 分配。
### R10-N4 P2 — MapRoomAutoRegister 无"显式默认 Database"配置
**位置**`MapRoomAutoRegister.cs:46-55`
逻辑:所有 Database 按 GUID 排序,**首个**作为默认。在跨团队/多 Database 场景下(如 DLC 扩展用独立 Database新建 Room 永远进主 Database策划需手动迁移。
**修复建议**
- 方案 A`MapDatabaseSO` 增加 `[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`
```csharp
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`
```csharp
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:19``MapPanel.ShowTooltip`
`RoomData.DisplayName` 是原始字符串。多语言版本需要每个 RoomData 维护多套字段或在 Tooltip 显示时调用 `LocalizationManager``RegionNameDisplay` 已经做了本地化映射,房间名应该对齐。
**修复建议**:增加 `[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`
```csharp
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` 增加:
```csharp
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性能/体验抛光)
3. **R10-N3** Pin 对象池
4. **R10-N12** 拆分 OnExplorationChanged / OnDatabaseChanged 事件语义
5. **R10-N5** 拖拽实时重叠预警
### 优先级 3可选增强
6. **R10-N4** 显式默认 Database 配置
7. **R10-N6** 多选框选拖拽
8. **R10-N7** AllRooms 封装为 property
9. **R10-N8** Input System 适配
10. **R10-N9** MapPlayerTracker `_isDuplicate` 显式化
11. **R10-N10** RoomData 本地化
12. **R10-N11** 大地图分帧构建
13. **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` / `_explorationDirty`OnEnable 检测脏标志补刷 |
| R10-N2 | MinimapHUD OnDisable 销毁全部 Cell | ✅ 完成 | Awake 中订阅 + 准备字典OnDisable 不再销毁;脏标志驱动延迟刷新 |
| R10-N3 | Pin 频繁 Instantiate/Destroy | ✅ 完成 | MapPanel & MinimapHUD 引入 `Stack<Image> _pinPool`ClearPins → SetActive(false) 回收OnDestroy 销毁池 |
| R10-N4 | 多 Database 自动选择规则不显式 | ✅ 完成 | MapDatabaseSO 新增 `IsDefault`AutoRegister 用 `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 新增 `OnExplorationChanged`Load/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.csproj`**0 警告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 + 美术资产部分占用)