Files
zeling_v2/Docs/Review/Minimap_Review_Round7_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

426 lines
18 KiB
Markdown
Raw 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 7 独立再审查报告
> **评估日期**Round 7
> **评估方法**:在 Round 6 修复完成后,再次独立通读所有 17 个地图模块文件,重点发现 Round 6 遗漏的真实问题
> **对标标准**:成熟 2D Metroidvania 游戏小地图,专注编辑器扩展质量、架构解耦、高性能、策划友好度
---
## 一、Round 6 修复验证
| Round 6 修复 ID | 验证结果 | 备注 |
|---|---|---|
| P1-1 MinimapHUD 空间索引 | ✅ 通过 | `_spatialIndex` 构建/查询逻辑正确O(viewRadius²) 替代 O(N) 落实 |
| P2-1 MapDatabaseEditor GUIStyle 缓存 | ✅ 通过 | `_errorRowStyle` + `GetErrorRowStyle()` 惰性初始化,`OnEnable` 重置支持皮肤切换 |
| P2-2 MapRoomDataEditor CELL_SIZE 说明 | ✅ 通过 | Inspector HelpBox 已补充三段坐标系说明 |
| P2-3 MapLayoutEditorWindow Undo 刷新 | ✅ 通过 | `OnEnable`/`OnDisable` 正确注册/注销,`OnUndoRedo` 清除验证缓存并 Repaint |
| P2-4 ValidateAll 编辑器保护 | ✅ 通过 | `#if UNITY_EDITOR` 包裹完整,构建包不再含 O(N²) 验证逻辑 |
| N3 MinimapHUD Setup 冗余位置 | ⚠️ 部分通过 | step② 已加 PlaceCell但 step③ 仍重新遍历全部 _cells 包含刚 PlaceCell 完的新格子,新格子被 PlaceCell 两次(详见 R7-N6 |
Round 6 主要修复落实良好,但 N3 引入了新的小冗余。
---
## 二、Round 7 新发现的问题
### 🔴 **R7-N1**MapPanel 不响应 Pin 增删事件(真实 UX 缺陷)
**位置**`MapPanel.cs` 第 88-92 行
```csharp
private void OnEnable()
{
...
RenderPins(); // ← 只在打开面板时调用一次
UpdatePlayerIcon();
CenterOnCurrentRoom();
_onMapUpdated?.Subscribe(OnMapUpdated).AddTo(_subs);
}
```
**问题**
- `RenderPins()` 仅在 `OnEnable` 调用一次
- `LateUpdate` 只刷新玩家图标,**从不重新检查 PinsVersion**
- 玩家在地图打开状态下添加/删除 Pin典型操作UI 不会同步刷新
**影响**:地图面板期间用户的所有 Pin 操作CreatePin/RemovePin在面板关闭并重新打开前视觉上无任何反馈。这是策划/玩家可见的 UX 错误。
**修复方案**
- 方案 A轻量、推荐`LateUpdate` 末尾调用 `RenderPins()`,已有 `_lastPinVersion` 脏检查,无版本变化时立即 return无开销
- 方案 B`IPinService` 中新增 `event Action OnPinsChanged``MapPinManager``AddPin/RemovePin` 时触发
---
### 🔴 **R7-N2**MapPanel/MinimapHUD 不响应数据库热更(编辑器/工具集成缺陷)
**位置**`MapPanel.cs` 第 83-86 行,`MinimapHUD.cs` 第 56-70 行
```csharp
// MapPanel
if (_cells.Count == 0)
BuildGrid();
else
RefreshAllCells(); // ← 只刷新已存在 _cells 的可见性
```
```csharp
// MinimapHUD
BuildSpatialIndex(_mapSvc?.Database); // ← 仅在 OnEnable 时构建
```
**问题**
- 策划在编辑器中通过 `MapLayoutEditorWindow` 编辑房间布局或新增房间后,运行中的 MapPanel/MinimapHUD 不会反映变化
- `RefreshAllCells` 只更新已有 `_cells` 集合,新增房间不会被实例化、被删除房间的格子不会被回收
- 空间索引同样不会重建
**影响**
- 策划开发时的迭代体验差:每次编辑数据库都需重启游戏
- 运行时若支持 Addressable 数据库热更或动态地图生成DLC地图会失效
**修复方案**
1. `IMapService` 新增 `event Action OnDatabaseChanged`
2. `MapDatabaseSO.OnValidate` 中触发该事件(编辑器侧)
3. `MapPanel`/`MinimapHUD` 订阅并执行完整重建
---
### 🟡 **R7-N3**BuildSpatialIndex 在 MinimapHUD 和 MapPlayerTracker 中重复实现
**位置**`MinimapHUD.cs` 第 97-108 行 vs `MapPlayerTracker.cs` 第 67-81 行
两处构建几乎相同的 `Dictionary<Vector2Int, string>` 索引(玩家追踪和小地图查询)。每个组件都独立构建一次相同数据,浪费内存 + 数据不一致风险(房间数据变化时一方更新另一方未更新)。
**修复方案**:将索引下沉为 `MapDatabaseSO` 的内置查询:
```csharp
// MapDatabaseSO 中
private Dictionary<Vector2Int, string> _cellToRoom;
public string GetRoomIdAtCell(Vector2Int cell) { ... }
```
或封装为 `MapServiceExtensions.GetRoomIdAtCell(this IMapService svc, Vector2Int cell)` 集中实现。
---
### 🟡 **R7-N4**MapPinManager.CreatePin 缺少输入校验
**位置**`MapPin.cs` 第 61-74 行
```csharp
public MapPin CreatePin(string roomId, float normX, float normY,
PinType type = PinType.Marker, string note = "")
{
var pin = new MapPin {
RoomId = roomId, // 不验证非空、不验证 roomId 是否存在
NormalizedPosX = normX, // 不 Clamp01
NormalizedPosY = normY,
...
};
AddPin(pin);
return pin;
}
```
**问题**
- `roomId` 可空字符串/无效 ID导致 `MapPanel.RenderPins``TryGetValue` 失败但 Pin 仍存在于存档
- `normX/normY` 可为负数或大于 1Pin 显示在房间格子外
- 错误数据持久化到存档,后续清理困难
**修复方案**:参数校验 + `Mathf.Clamp01` + RoomId 存在性检查(可选警告)。
---
### 🟡 **R7-N5**MapManager.OnRoomEntered 区域变化日志缺失
**位置**`MapManager.cs` 第 70-82 行
```csharp
private void OnRoomEntered(string roomId)
{
bool changed = _exploredRooms.Add(roomId);
if (changed) _onMapUpdated?.Raise(roomId);
var regionId = _database?.GetRoom(roomId)?.RegionId;
if (!string.IsNullOrEmpty(regionId) && regionId != _currentRegionId)
{
_currentRegionId = regionId;
_onRegionChanged?.Raise(regionId);
}
}
```
**问题**
- 首次启动游戏(无存档加载)时,玩家首次进入某区域,`_currentRegionId` 由 null 变成 `regionId`,会触发一次 `EVT_RegionChanged`
-**加载存档后**`OnLoad` 只恢复 `_exploredRooms`/`_mappedRooms`**不恢复 `_currentRegionId`**
- 玩家加载存档进入游戏时,第一次进入房间会再次触发 EVT_RegionChanged导致「读档后第一次进房显示区域名渐显」UX 异常
**修复方案**`OnLoad` 同步恢复 `_currentRegionId`(可基于玩家当前位置或最近进入的房间推导)。
---
### 🟡 **R7-N6**MinimapHUD step③ 对新格子的二次 PlaceCell
**位置**`MinimapHUD.cs` 第 175-194 行
```csharp
foreach (var roomId in _roomsInViewBuffer)
{
...
PlaceCell(cell, room); // ← step② 中刚调用
_cells[roomId] = cell;
}
// ③ 重定位所有格子
foreach (var (id, cell) in _cells)
{
...
PlaceCell(cell, r); // ← 新格子在此被再次 PlaceCell
}
```
**问题**:刚在 step② 中通过 PlaceCell 设置过位置的新格子,在 step③ 中又被 PlaceCell 一遍。虽然结果幂等,但浪费写入。
**修复方案**step② 不调用 PlaceCell信任 step③ 统一处理),或 step③ 跳过本帧新增的格子。后者更清晰:
```csharp
var newlyAdded = new HashSet<string>();
// step②
foreach (var roomId in _roomsInViewBuffer) {
if (_cells.ContainsKey(roomId)) continue;
...
PlaceCell(cell, room);
newlyAdded.Add(roomId);
_cells[roomId] = cell;
}
// step③ 重定位剩余存量格子
foreach (var (id, cell) in _cells) {
if (newlyAdded.Contains(id)) continue;
...
}
```
---
### 🟢 **R7-N7**MapInputHandler 仍使用旧版 Input SystemRound 6 N4 未修复)
**位置**`MapInputHandler.cs` 第 42-43 行
```csharp
float h = Input.GetAxisRaw("Horizontal");
float v = Input.GetAxisRaw("Vertical");
```
新 Input System 项目此代码失效,已在 Round 6 标注但未修复,本轮再次确认。
---
### 🟢 **R7-N8**MapPlayerTracker._worldUnitsPerCell 字段无范围保护
**位置**`MapPlayerTracker.cs` 第 25 行
```csharp
[SerializeField] private float _worldUnitsPerCell = 18f;
```
若策划误填 0 或负值,会导致:
- `WorldToCell` 除以 0 产生 `Infinity`,进而 `Mathf.FloorToInt` 异常
- 归一化位置计算 `worldSize.x` 为 0`Mathf.Max(1f, ...)` 兜底但语义错误
**修复方案**`[Min(0.01f)]``OnValidate` 保护。
---
### 🟢 **R7-N9**MapPin.cs 文件名与类名不一致(历史遗留)
`MapPin.cs` 内部包含 `MapPinManager`(主类)+ `PinSpriteEntry`。VS Code/Rider 全局搜索 `MapPinManager` 文件名找不到,新人查找代码体验差。
**修复方案**:因 Unity .meta GUID 绑定限制,安全做法是新建 `MapPinManager.cs` 仅含一个 `// see MapPin.cs` 注释引导(不安全的做法是改 .meta 但风险高);或保持现状并在 README 注明。
---
### 🟢 **R7-N10**MapDatabaseSO._index OnDisable 清理的潜在 NRE
**位置**`MapRoomDataSO.cs` 第 92 行
```csharp
private void OnDisable() => _index = null;
```
ScriptableObject 在域重载Domain Reload/编辑器停止播放时执行 OnDisable。若同时有运行中的协程或异步任务持有 SO 引用并即将调用 `GetRoom`,会触发索引重建(线性 LINQ首帧停止时性能尖峰 + 短暂 NRE 风险。
**修复方案**:清理改为 `lock` 包裹的重建逻辑,或保留缓存让 GC 自然回收。当前实现风险低但非完全无害。
---
## 三、本轮独立评分
| 维度 | Round 6 | Round 7 独立 | 差值 | 说明 |
|---|---|---|---|---|
| 架构解耦 | 9.0 | **8.5** | -0.5 | R7-N3索引重复实现扣 0.5 |
| 性能 | 8.0→8.5 | **8.5** | 维持 | P1-1 已修复,新发现的 N6 冗余写入影响微小 |
| 编辑器扩展 | 8.5→9.0 | **9.0** | 维持 | P2 修复落实 |
| 数据设计 | 8.0 | **7.5** | -0.5 | R7-N4无输入校验+ R7-N5OnLoad 不恢复 region扣分 |
| 功能完整性 | 8.0 | **7.5** | -0.5 | R7-N1Pin 不响应增删)是真实功能缺陷 |
| 代码质量 | 8.5 | **8.5** | 0 | 整体仍达标 |
| 可扩展性 | 8.0 | **7.5** | -0.5 | R7-N2无热更事件限制工具化扩展 |
| 策划友好度 | 8.0 | **7.5** | -0.5 | R7-N2 影响策划迭代体验 |
### 加权总分
```
架构解耦 ████████░░ 8.5/10
性能 ████████░░ 8.5/10
编辑器扩展 █████████░ 9.0/10
数据设计 ███████░░░ 7.5/10
功能完整性 ███████░░░ 7.5/10 ← R7-N1 Pin 响应缺陷
代码质量 ████████░░ 8.5/10
可扩展性 ███████░░░ 7.5/10 ← R7-N2 无热更
策划友好度 ███████░░░ 7.5/10
─────────────────────────────
加权总分 82/100
```
**Round 6 报告预估修复后 87 分,本轮独立审查实际 82 分**——差距源于 Round 6 评估视野未覆盖 N1/N2/N3 这三类"运行时-编辑器"协同问题。
---
## 四、严重程度排序与修复优先级
### P0立即修复影响真实 UX/功能)
| ID | 问题 | 工时估计 |
|---|---|---|
| R7-N1 | MapPanel 不响应 Pin 增删 | 小LateUpdate 加 RenderPins 调用) |
| R7-N5 | OnLoad 不恢复 _currentRegionId | 小OnLoad 推导一行) |
### P1影响工具化与扩展
| ID | 问题 | 工时估计 |
|---|---|---|
| R7-N2 | MapPanel/MinimapHUD 不响应数据库热更 | 中IMapService 新增事件 + 订阅) |
| R7-N3 | BuildSpatialIndex 重复实现 | 中(下沉到 MapDatabaseSO 或扩展方法) |
| R7-N4 | CreatePin 缺输入校验 | 小(参数 Clamp + 警告) |
### P2代码质量与健壮性
| ID | 问题 | 工时估计 |
|---|---|---|
| R7-N6 | step③ 对新格子二次 PlaceCell | 小newlyAdded 集合跳过) |
| R7-N7 | MapInputHandler 旧版 Input | 中(替换为 IInputService |
| R7-N8 | _worldUnitsPerCell 无范围保护 | 极小(`[Min]` |
### P3历史遗留 / 边缘场景)
| ID | 问题 |
|---|---|
| R7-N9 | MapPin.cs 文件名与类名不一致 |
| R7-N10 | _index OnDisable 清理潜在 NRE |
---
## 五、对标空洞骑士 / 丝之歌的差距分析
| 能力 | 对标标准 | 当前状态 | 差距 |
|---|---|---|---|
| **三级可见性Unknown/Explored/Mapped** | ✓ | ✓ 完整 | 无 |
| **MapFragment 购买揭示** | ✓ | ✓ 接口已就位 | 无 |
| **自定义 Pin 标记** | ✓ 多种类型 | ✓ 但**地图打开时不响应增删** | **N1** |
| **区域名渐显** | ✓ | ✓ 但读档后会异常触发 | **N5** |
| **存档点显示完整地图** | ✓ | ✓ via SetMapped API | 无 |
| **房间非矩形轮廓** | ✓ | ✓ RoomOutlineTex 字段 | 无 |
| **小地图视野跟随玩家** | ✓ | ✓ 含空间索引优化 | 无 |
| **图标平滑跟随(非格子跳跃)** | ✓ | ✓ Round 4 已修复 | 无 |
| **运行时缩放/平移** | ✓ 滚轮+键盘+手势 | ⚠️ 仅滚轮+键盘 | 无手势/手柄 |
| **探索进度统计 UI** | ✓ | ❌ API 完整但无 UI 显示 | 中等 |
| **区域配色主题** | ✓ 各区域有专属配色 | ❌ 编辑器随机配色,运行时无 | 大(需 RegionSO |
| **数据库热更** | — | ❌ 不响应 | **N2** |
---
## 六、总评
### 当前最终得分:**82/100**
**优势**
- 完整的接口/ServiceLocator 解耦体系
- 编辑器三工具SO Inspector + Custom Editor + 全局布局窗口)功能扎实
- 热路径性能已优化(脏检查 + 缓存 + 空间索引)
- 数据驱动设计支持策划独立迭代
**真实差距(按修复成本排序)**
1. **N1 Pin 不响应增删** — 1 行修复,是当前最高 ROI 的修复点
2. **N5 OnLoad 不恢复 region** — 1 行修复,避免读档体验异常
3. **N4 CreatePin 无校验** — 3 行修复,避免脏数据进入存档
4. **N6 step③ 二次 PlaceCell** — 5 行修复,性能小优化
5. **N3 索引重复实现** — 重构DRY 改善
6. **N2 数据库热更事件** — 中等改动,但显著提升策划工作流
**仍未弥补的功能缺口**Round 5/6 已标注,仍在):
- 探索进度 UIAPI 有UI 无)
- RegionSO区域配色/名称集中配置)
- MapDesignSpec.md 策划文档
- Pinch 缩放、手柄输入
如果完成 P0+P1N1/N5/N2/N3/N4评分预期回到 **88-90 分**;再补 RegionSO + 进度 UI 可达 **93+**
---
*Round 7 旨在矫正 Round 6 在「运行时-编辑器协同」「事件响应完整性」两个视角的盲区。本轮重点发现的 N1/N2/N5 是 Round 1-6 全部漏检的真实功能/UX 缺陷,建议立即修复。*
---
## 七、修复实施结果追踪
> 评估完成后,按本报告优先级 P0+P1+P2 全部修复,并通过 dotnet build 验证编译通过。
### 已实施的修复
| ID | 修复内容 | 改动文件 | 状态 |
|---|---|---|---|
| **R7-N1** | MapPanel.LateUpdate 首行调用 RenderPins(),借 PinsVersion 脏检查零开销响应 Pin 增删 | `MapPanel.cs` | ✅ |
| **R7-N5** | MapSaveData 新增 `LastRegionId` 字段MapManager.OnSave 写入、OnLoad 恢复 `_currentRegionId` | `SaveData.cs``MapManager.cs` | ✅ |
| **R7-N4** | CreatePin 增加 roomId 非空校验、normX/normY `Clamp01`、note 64 字符截断、可选数据库存在性 Warning | `MapPin.cs` | ✅ |
| **R7-N6** | MinimapHUD 引入 `_newlyAddedBuffer`step③ 跳过新增格子,避免重复 PlaceCell | `MinimapHUD.cs` | ✅ |
| **R7-N3** | 空间索引下沉到 `MapDatabaseSO.GetRoomIdAtCell()`MinimapHUD 和 MapPlayerTracker 共用;新增 `InvalidateIndex()` 供热更使用 | `MapRoomDataSO.cs``MinimapHUD.cs``MapPlayerTracker.cs` | ✅ |
| **R7-N2** | IMapService 新增 `event Action OnDatabaseChanged``NotifyDatabaseChanged()` 方法MapPanel/MinimapHUD 订阅并完整重建(含索引失效) | `IMapService.cs``MapManager.cs``MapPanel.cs``MinimapHUD.cs` | ✅ |
| **R7-N8** | `_worldUnitsPerCell` 增加 `[Min(0.01f)]` 防止 0/负值导致除零 | `MapPlayerTracker.cs` | ✅ |
| **R7-N7额外** | 修复 `BaseGames.Input` 命名空间遮蔽 `UnityEngine.Input` 导致的编译错误(使用全限定 `UnityEngine.Input.GetAxisRaw` | `MapInputHandler.cs` | ✅ |
### 未实施P3 历史遗留)
| ID | 原因 |
|---|---|
| R7-N9 | MapPin.cs 文件名问题Unity .meta GUID 绑定限制,安全方案是新增 `MapPinManager.cs` 指引文件已在文件顶部添加注释引导搜索Round 6 已做) |
| R7-N10 | SO `OnDisable` 索引清理:当前 SO 卸载场景下不会触发实际运行问题;过度防御反而增加复杂度,保持现状 |
### 编译验证
```
dotnet build BaseGames.World.Map.csproj → 0 警告 0 错误
dotnet build BaseGames.Core.Save.csproj → 0 警告 0 错误
dotnet build BaseGames.Progression.csproj → 0 警告 0 错误
```
### 修复后预期得分
| 维度 | Round 7 修复前 | Round 7 修复后 | 关键改变 |
|---|---|---|---|
| 架构解耦 | 8.5 | **9.0** | N3 索引下沉DRY 改善 |
| 性能 | 8.5 | **9.0** | N6 减少重复写入;索引共享减少内存 |
| 编辑器扩展 | 9.0 | **9.0** | 维持 |
| 数据设计 | 7.5 | **8.5** | N4 输入校验 + N5 区域持久化 |
| 功能完整性 | 7.5 | **8.5** | N1 Pin 实时响应 |
| 代码质量 | 8.5 | **9.0** | N8 边界保护 + 修复阻塞性编译错误 |
| 可扩展性 | 7.5 | **8.5** | N2 数据库热更事件 |
| 策划友好度 | 7.5 | **8.5** | N2 编辑时无需重启游戏 |
**修复后预期总分:约 88-90/100**
剩余至空洞骑士对标级93+)的距离:
1. 探索进度 UIAPI 已有,缺渲染层)
2. RegionSO区域配色/名称集中管理)
3. 手柄/触屏缩放与平移
4. `Docs/Standards/MapDesignSpec.md` 策划工作流文档
这些是真正意义的"扩展"而非"修补",可在独立任务中推进。