- Summarized the evolution of scores across five review rounds - Detailed the status of each evaluation dimension post-fixes - Highlighted remaining issues and recommended future work for further enhancements - Compared current system against industry benchmarks
446 lines
20 KiB
Markdown
446 lines
20 KiB
Markdown
# 小地图系统重审报告(第四轮 · 独立质疑审查)
|
||
|
||
> **项目**:zeling_v2
|
||
> **审查范围**:全部 14 个 Map 系统文件(与第三轮相同)
|
||
> **审查目的**:独立质疑第三轮评估(89/100),验证结论是否受"自评偏差"影响
|
||
> **评估基准**:成熟 2D 银河恶魔城(对标《空洞骑士》《丝之歌》)+ 专业编辑器扩展标准
|
||
> **审查轮次**:第四轮
|
||
|
||
---
|
||
|
||
## 一、总结性裁定
|
||
|
||
**第三轮评分 89/100 存在系统性高估。**
|
||
|
||
经逐行重审,发现以下问题:
|
||
- **第三轮完全遗漏**的关键缺陷:6 项
|
||
- **第三轮提及但严重低估扣分**的缺陷:5 项
|
||
- **新发现正确性 Bug**:2 项(含运行时玩家图标离散跳动 + 编辑器验证错误归因误判)
|
||
|
||
修订后总分:**79 / 100**(降幅 -10 分)
|
||
|
||
---
|
||
|
||
## 二、修订评分总表
|
||
|
||
| 评估维度 | 满分 | 第三轮 | 本轮 | 差值 | 主因 |
|
||
|----------|------|--------|------|------|------|
|
||
| 1. 架构解耦与接口设计 | 20 | 19 | **15** | -4 | 直接具体类依赖(2处);无 IPinService / IPlayerPositionProvider |
|
||
| 2. 运行时性能 | 15 | 14 | **11** | -3 | Canvas.ForceUpdateCanvases;LateUpdate 路径 GetComponent 未缓存;RenderPins 无脏检查 |
|
||
| 3. 编辑器扩展工具 | 15 | 13 | **11** | -2 | GUIStyle 每帧 new;errorSet 每帧重建 O(N²);验证结果存在 RoomId 前缀误判 Bug |
|
||
| 4. 数据模型完整性 | 10 | 9 | **7** | -2 | 32f 魔法数字分散三处;_index 不在 OnValidate 中清除;无 RegionSO |
|
||
| 5. 策划/开发友好度 | 10 | 9 | **8** | -1 | 两套像素比例(32f/16f)缺文档;_worldUnitsPerCell 设置无指引 |
|
||
| 6. 小地图功能对标 | 15 | 11 | **9** | -2 | 玩家图标在房间内**离散跳动**(非平滑);缺探索进度 UI |
|
||
| 7. 代码质量与可维护性 | 10 | 9 | **8** | -1 | GetComponent 无 TryGetComponent(3 处);PinSpriteEntry 位置不当 |
|
||
| 8. 存档/持久化 | 5 | 5 | **5** | 0 | — |
|
||
| **总计** | **100** | **89** | **79** | **-10** | |
|
||
|
||
---
|
||
|
||
## 三、逐项问题详述
|
||
|
||
---
|
||
|
||
### 🔴 严重问题(对用户可见质量影响大)
|
||
|
||
---
|
||
|
||
#### 问题 1:玩家图标在房间内只能离散跳动(功能对标 -2)
|
||
|
||
**文件**:`MapPlayerTracker.cs`,第 80–83 行
|
||
|
||
```csharp
|
||
Vector2 inRoom = (Vector2)(cellPos - room.GridPosition); // cellPos 是整数格坐标
|
||
NormalizedPositionInRoom = new Vector2(
|
||
inRoom.x / Mathf.Max(1, room.GridSize.x),
|
||
inRoom.y / Mathf.Max(1, room.GridSize.y));
|
||
```
|
||
|
||
`cellPos` 是当前所在**网格单元**(整数),不是世界坐标浮点值。对一个 3×2 的房间,
|
||
`NormalizedPositionInRoom` 仅能取 **6 个离散值**:`(0, 0)、(0.33, 0)、(0.67, 0)、(0, 0.5)、(0.33, 0.5)、(0.67, 0.5)`。
|
||
|
||
- 玩家在房间内移动时,图标在地图上**以网格格为步长跳动**,而非平滑跟随
|
||
- 《空洞骑士》中地图标记跟随玩家实际世界位置平滑移动
|
||
- **第三轮报告完全未提及此问题**,却在"功能对标"维度给出 11/15
|
||
|
||
**修复方向**:
|
||
|
||
```csharp
|
||
// LateUpdate 中:在已知当前房间后,用世界坐标精确计算归一化位置
|
||
Vector2 worldMin = new Vector2(room.GridPosition.x * _worldUnitsPerCell,
|
||
room.GridPosition.y * _worldUnitsPerCell);
|
||
Vector2 worldSize = new Vector2(room.GridSize.x * _worldUnitsPerCell,
|
||
room.GridSize.y * _worldUnitsPerCell);
|
||
Vector2 localPos = (Vector2)_playerTransform.position - worldMin;
|
||
NormalizedPositionInRoom = new Vector2(
|
||
Mathf.Clamp01(localPos.x / worldSize.x),
|
||
Mathf.Clamp01(localPos.y / worldSize.y));
|
||
```
|
||
|
||
此修改同时无需缓存 cellPos,可降低脏检查复杂度。
|
||
|
||
---
|
||
|
||
#### 问题 2:`MapPanel` 和 `MinimapHUD` 直接依赖具体类,破坏 ServiceLocator 模式(架构 -4)
|
||
|
||
**文件**:`MapPanel.cs` 第 35 行、第 41 行;`MinimapHUD.cs` 第 22 行
|
||
|
||
```csharp
|
||
// MapPanel.cs
|
||
[SerializeField] private MapPlayerTracker _playerTracker; // 具体类,应为 IPlayerPositionProvider
|
||
[SerializeField] private MapPinManager _pinManager; // 具体类,应为 IPinService
|
||
|
||
// MinimapHUD.cs
|
||
[SerializeField] private MapPlayerTracker _playerTracker; // 同上
|
||
```
|
||
|
||
整个系统通过 `ServiceLocator + IMapService` 实现了良好解耦,但这两处具体类直接引用完全绕开了该模式:
|
||
|
||
1. **没有 `IPlayerPositionProvider` 接口**:无论测试还是日后扩展(多人、观察者模式、重播系统),都必须提供一个真实的 `MapPlayerTracker` 组件——不可 mock,场景耦合
|
||
2. **没有 `IPinService` 接口**:`MapPanel` 直接调用 `_pinManager.Pins`,意味着切换 pin 实现必须修改 `MapPanel`
|
||
3. **第三轮报告将此归纳为"轻微双重依赖"只扣 1 分**(-1);实际情况是两组件各有此问题,合计影响更大
|
||
|
||
**第三轮打分 19/20 不合理;修订为 15/20。**
|
||
|
||
---
|
||
|
||
#### 问题 3:`Canvas.ForceUpdateCanvases()` 每次打开地图时触发(性能 -1.5)
|
||
|
||
**文件**:`MapPanel.cs` 第 206 行
|
||
|
||
```csharp
|
||
Canvas.ForceUpdateCanvases(); // 全量 Canvas 布局重建
|
||
```
|
||
|
||
`CenterOnCurrentRoom()` 在每次 `OnEnable` 时调用此方法。
|
||
- 这是 Unity UI 中**最昂贵的单次 API 调用之一**,触发整个 Canvas 树的全量布局计算
|
||
- 在拥有大量 UI 元素的游戏中(HUD / 对话框 / 背包叠加 Canvas),单次耗时可超过 2ms
|
||
- **第三轮报告记录此问题但依然给出 14/15**,表明评分未真正反映其严重性
|
||
|
||
**修复方向**:
|
||
|
||
```csharp
|
||
// 将强制布局限定在 ScrollRect 的 content 节点
|
||
LayoutRebuilder.ForceRebuildLayoutImmediate(_scrollRect.content);
|
||
```
|
||
|
||
---
|
||
|
||
### 🟠 重要问题(影响代码质量和扩展性)
|
||
|
||
---
|
||
|
||
#### 问题 4:`LateUpdate` 热路径中 `GetComponent<RectTransform>()` 未缓存(性能 -1)
|
||
|
||
**文件**:`MapPanel.cs` 第 178 行、第 212 行、第 243 行
|
||
|
||
```csharp
|
||
// UpdatePlayerIcon():LateUpdate 触发,脏检查通过后每次执行
|
||
var cellRT = cell.GetComponent<RectTransform>(); // 第 178 行,未缓存
|
||
|
||
// CenterOnCurrentRoom():每次地图打开
|
||
var cellRT = cell.GetComponent<RectTransform>(); // 第 212 行
|
||
|
||
// RenderPins():每次地图打开
|
||
var cellRT = cell.GetComponent<RectTransform>(); // 第 243 行
|
||
```
|
||
|
||
`MapRoomCellUI` 已有 `TryGetComponent<RectTransform>()` 调用(`Setup()` 内),说明团队意识到需要通过组件获取 RT;但热路径中的三处均未缓存。
|
||
|
||
`MinimapHUD.UpdatePlayerDot()` 正确使用了 `TryGetComponent`,但 `MapPanel` 全部用的是 `GetComponent`——代码风格不一致。
|
||
|
||
**修复**:在 `MapRoomCellUI` 中暴露 `public RectTransform RT { get; private set; }` 并在 `Awake` 赋值。
|
||
|
||
---
|
||
|
||
#### 问题 5:`RenderPins()` 每次 `OnEnable` 全量销毁重建,无脏检查(性能 -0.5)
|
||
|
||
**文件**:`MapPanel.cs` 第 234–249 行
|
||
|
||
```csharp
|
||
private void RenderPins()
|
||
{
|
||
ClearPins(); // 每次打开地图:Destroy 全部 Image
|
||
if (_pinPrefab == null || _pinManager == null) return;
|
||
foreach (var pin in _pinManager.Pins) // 重新 Instantiate
|
||
{ ... }
|
||
}
|
||
```
|
||
|
||
相比之下,格子在首次打开后缓存复用(`if (_cells.Count == 0) BuildGrid()`)。pins 没有相同处理:每次打开地图都执行 N 次 `Destroy` + N 次 `Instantiate`。
|
||
|
||
玩家在同一区域频繁打开关闭地图时,这是不必要的开销,且会触发 GC。
|
||
|
||
---
|
||
|
||
#### 问题 6:`RunValidation()` 中 RoomId 前缀误判 Bug(编辑器正确性 -1)
|
||
|
||
**文件**:`MapLayoutEditorWindow.cs` 第 319–323 行;`MapDatabaseEditor.cs` 第 106–109 行
|
||
|
||
```csharp
|
||
foreach (var r in _database.AllRooms)
|
||
if (r != null && err.Contains(r.RoomId)) // ← 子字符串匹配
|
||
_errorRoomIds.Add(r.RoomId);
|
||
```
|
||
|
||
当 `RoomId = "Room_A1"` 时,针对 `"Room_A10"` 的错误字符串同样包含 `"Room_A1"`,导致:
|
||
- `"Room_A1"` 被错误地标为红色(误报)
|
||
- 开发人员可能花时间排查并不存在的问题
|
||
|
||
**第三轮报告只提到 O(N²) 性能,未指出此正确性 Bug。**
|
||
|
||
**修复**:改用边界匹配(单引号括号检查):
|
||
|
||
```csharp
|
||
// ValidateAll 生成错误时改用带引号的格式(已有实现):
|
||
// $"RoomId '{room.RoomId}' 重复 ..."
|
||
// 匹配时改为:err.Contains($"'{r.RoomId}'")
|
||
```
|
||
|
||
---
|
||
|
||
#### 问题 7:`MapDatabaseEditor` 中 `errorSet` 在每帧 OnGUI 中重建(编辑器性能 -0.5)
|
||
|
||
**文件**:`MapDatabaseEditor.cs` 第 101–110 行
|
||
|
||
```csharp
|
||
// OnInspectorGUI() 每次 Inspector 重绘都执行(Unity 高频调用此方法)
|
||
bool errorSetBuilt = false;
|
||
var errorSet = new HashSet<string>();
|
||
if (_lastErrors != null)
|
||
{
|
||
errorSetBuilt = true;
|
||
foreach (var err in _lastErrors)
|
||
foreach (var r in _database.AllRooms) // O(E × N) 每帧
|
||
if (r != null && err.Contains(r.RoomId))
|
||
errorSet.Add(r.RoomId);
|
||
}
|
||
```
|
||
|
||
房间列表展开时,每次 Inspector 重绘(包括鼠标移动触发的重绘)都重新执行 O(E × N) 的字符串扫描。
|
||
`_lastErrors` 并不会在重绘间改变——此计算应在 `_lastErrors` 被赋值时一次性完成。
|
||
|
||
---
|
||
|
||
#### 问题 8:`GUIStyle` 在 `DrawMapArea()` 内每帧 `new`(编辑器性能 -0.5)
|
||
|
||
**文件**:`MapLayoutEditorWindow.cs` 第 190–197 行、第 218–224 行
|
||
|
||
```csharp
|
||
// OnGUI 中,DrawMapArea 每帧调用:
|
||
var style = new GUIStyle(EditorStyles.miniLabel) // 每帧 new,每房间 1 次
|
||
{
|
||
alignment = TextAnchor.MiddleCenter,
|
||
...
|
||
fontSize = Mathf.Clamp(Mathf.RoundToInt(_zoom * 0.4f), 8, 14),
|
||
};
|
||
```
|
||
|
||
对一个包含 100 个房间的数据库,每帧分配 200+ 个 `GUIStyle` 对象(标签 + badge 各一)。在交互式编辑器窗口中,每秒重绘可达 60 次,意味着每秒 ~12,000 次堆分配。
|
||
Editor 窗口缩放/拖拽时会出现明显卡顿。
|
||
|
||
**修复**:将 style 缓存为字段,仅当 `_zoom` 变化时更新 `fontSize`。
|
||
|
||
---
|
||
|
||
### 🟡 中等问题(影响可维护性)
|
||
|
||
---
|
||
|
||
#### 问题 9:`32f` 魔法数字分散三处,与 `MinimapHUD._cellPixels` 两套比例无共享常量(数据模型 -1)
|
||
|
||
```
|
||
MapRoomCellUI.Setup() 行 43:room.GridPosition.x * 32f
|
||
MapRoomCellUI.Setup() 行 44:room.GridSize.x * 32f
|
||
MapPanel.DrawExits() 行 138:const float px = 32f
|
||
MinimapHUD._cellPixels 默认值 16f(Inspector 可调)
|
||
```
|
||
|
||
- `Setup()` 写入的 32f 位置**立即被 `MinimapHUD.PlaceCell()` 覆盖**,造成双写浪费
|
||
- `MapPanel` 的 `DrawExits` 使用的 32f 与 `MapRoomCellUI.Setup()` 没有共享来源
|
||
- 如果调整格子像素大小,需要修改 3 处,且无编译时保障
|
||
|
||
**修复**:提取 `MapGridConstants.FullMapCellPixels = 32f`,Setup 接收 `pixelsPerCell` 参数。
|
||
|
||
---
|
||
|
||
#### 问题 10:`MapDatabaseSO._index` 在 `OnValidate` 不清除(数据模型 -1)
|
||
|
||
**文件**:`MapRoomDataSO.cs` / `MapDatabaseSO.cs`
|
||
|
||
`MapDatabaseSO.GetRoom()` 使用懒建 `_index`,但只在 `OnDisable` 中清除。
|
||
在编辑器中修改 `AllRooms`(添加/删除/重排房间 SO)后,`_index` 仍持有旧数据,直到 SO 卸载(通常是下次进入 Play Mode)。
|
||
此期间所有调用 `GetRoom()` 的系统都可能拿到过期结果。
|
||
|
||
**修复**:
|
||
|
||
```csharp
|
||
private void OnValidate() => _index = null; // 强制下次访问时重建
|
||
```
|
||
|
||
---
|
||
|
||
#### 问题 11:`PinSpriteEntry` 定义在 `MapPanel.cs`,位置不当(代码质量 -0.5)
|
||
|
||
**文件**:`MapPanel.cs` 第 290–295 行
|
||
|
||
```csharp
|
||
[Serializable]
|
||
public struct PinSpriteEntry
|
||
{
|
||
public PinType PinType;
|
||
public Sprite Sprite;
|
||
}
|
||
```
|
||
|
||
`PinType` 定义在 `MapPin.cs`,`PinSpriteEntry` 理应与之同在,或独立成 `MapPinTypes.cs`。
|
||
目前的位置导致任何需要引用 `PinSpriteEntry` 的扩展代码都需要依赖 `MapPanel` 的命名空间。
|
||
|
||
---
|
||
|
||
#### 问题 12:`GetPinSprite()` O(N) 线性扫描(性能 -0.5)
|
||
|
||
**文件**:`MapPanel.cs` 第 281–286 行
|
||
|
||
```csharp
|
||
foreach (var e in _pinSprites)
|
||
if (e.PinType == type) return e.Sprite;
|
||
```
|
||
|
||
每次渲染 pin 时线性扫描。
|
||
正确做法是在 `Awake/OnEnable` 时将 `_pinSprites` 转换为 `Dictionary<PinType, Sprite>`。
|
||
|
||
---
|
||
|
||
### 🔵 轻微问题(不影响当前功能,但影响长期可维护性)
|
||
|
||
---
|
||
|
||
#### 问题 13:两套像素比例(32f vs 16f)对策划/开发缺乏文档(友好度 -1)
|
||
|
||
`MapPanel` 格子像素固定 32f(代码写死),`MinimapHUD._cellPixels` 默认 16f(Inspector 可调)。
|
||
没有任何注释或文档说明:
|
||
- 两者为何不同
|
||
- 调整 `_cellPixels` 是否需要同步调整其他设置
|
||
- `_worldUnitsPerCell`(MapPlayerTracker)与格子像素的对应关系
|
||
|
||
新加入的策划/程序看到两个数字,不清楚它们的依赖关系,容易出错。
|
||
|
||
---
|
||
|
||
#### 问题 14:`MapPlayerTracker._worldUnitsPerCell` 缺乏设置指引(友好度 -0.5)
|
||
|
||
`_worldUnitsPerCell = 18f` 是一个关键参数——它直接决定了地图网格与游戏世界的对应关系。
|
||
Inspector 仅有一个 Header "世界坐标 → 格子坐标换算参数",但没有说明如何测量此值、如何与关卡设计统一。
|
||
建议在 Tooltip 中说明测量方法,或链接到 Docs 中的相关设计规范。
|
||
|
||
---
|
||
|
||
## 四、第三轮报告"自评偏差"分析
|
||
|
||
第三轮评估在两轮改进之后由实现者自评,以下模式表明存在**自评偏差(Implementation Bias)**:
|
||
|
||
| 问题 | 第三轮处理 | 独立重审结论 |
|
||
|------|-----------|-------------|
|
||
| 玩家图标离散跳动 | **未提及** | 关键功能对标缺陷 |
|
||
| MapPanel 具体类依赖 | "轻微双重依赖" -1 | 两处具体类依赖,-4 |
|
||
| Canvas.ForceUpdateCanvases | 列为"已知未修复",未减分 | 性能扣 -1.5 |
|
||
| GUIStyle 每帧 new | **未提及** | 编辑器性能扣 -0.5 |
|
||
| errorSet 每帧重建 | **未提及** | 编辑器性能扣 -0.5 |
|
||
| RunValidation 前缀误判 Bug | 只提 O(N²),未提正确性风险 | 正确性 Bug 扣 -1 |
|
||
| 32f 三处分散无共享常量 | **未提及** | 数据模型扣 -1 |
|
||
| _index 不在 OnValidate 清除 | 第三轮"已知"但未减分 | 数据模型扣 -1 |
|
||
|
||
---
|
||
|
||
## 五、修订后优先改进清单
|
||
|
||
按影响大小排序:
|
||
|
||
| 优先级 | 问题 | 文件 | 预估工时 |
|
||
|--------|------|------|---------|
|
||
| P0 | 修复 NormalizedPositionInRoom 为世界坐标插值 | MapPlayerTracker.cs | 0.5h |
|
||
| P0 | Canvas.ForceUpdateCanvases → LayoutRebuilder scoped | MapPanel.cs | 0.5h |
|
||
| P1 | 引入 IPlayerPositionProvider 接口 | 新文件 + MapPanel + MinimapHUD | 2h |
|
||
| P1 | 引入 IPinService 接口 | 新文件 + MapPin + MapPanel | 1.5h |
|
||
| P1 | 修复 RunValidation 前缀误判(改用 `'${id}'` 匹配) | MapLayoutEditorWindow.cs + MapDatabaseEditor.cs | 0.5h |
|
||
| P1 | MapDatabaseSO.OnValidate 清除 _index | MapRoomDataSO.cs | 0.1h |
|
||
| P2 | 提取 MapGridConstants.FullMapCellPixels,Setup() 接收参数 | MapRoomCellUI.cs + 调用方 | 1h |
|
||
| P2 | 缓存 GUIStyle,仅在 _zoom 变化时刷新 | MapLayoutEditorWindow.cs | 0.5h |
|
||
| P2 | MapDatabaseEditor:errorSet 缓存,不在每帧重建 | MapDatabaseEditor.cs | 0.5h |
|
||
| P2 | MapRoomCellUI 暴露 RT 属性;MapPanel 移除 GetComponent | MapRoomCellUI.cs + MapPanel.cs | 0.5h |
|
||
| P3 | RenderPins 增加脏检查,避免每次地图打开重建 | MapPanel.cs | 0.5h |
|
||
| P3 | GetPinSprite 改为 Dictionary 查找 | MapPanel.cs | 0.2h |
|
||
| P3 | MinimapHUD._toRemove 复用 List | MinimapHUD.cs | 0.1h |
|
||
| P3 | PinSpriteEntry 移至 MapPin.cs | MapPanel.cs + MapPin.cs | 0.2h |
|
||
|
||
---
|
||
|
||
## 六、架构图(修订后目标状态)
|
||
|
||
```
|
||
[IMapService] ← ServiceLocator ← [MapManager (ISaveable)]
|
||
↑ ↓ event
|
||
[MapPanel] [IPlayerPositionProvider] ← [MapPlayerTracker]
|
||
[MinimapHUD] ←── ServiceLocator ──→ ↑
|
||
(新接口)
|
||
[MapPanel] ←── ServiceLocator ──→ [IPinService] ← [MapPinManager]
|
||
(新接口)
|
||
```
|
||
|
||
---
|
||
|
||
## 八、修复实施结果追踪(第四轮审查后)
|
||
|
||
> **修复完成时间**:2026-05-25
|
||
> **修复执行状态**:全部 14 项问题已完成代码修复
|
||
|
||
| 优先级 | 问题 | 修复方案 | 状态 |
|
||
|--------|------|---------|------|
|
||
| P0 | 玩家图标离散跳动(NormalizedPositionInRoom) | `MapPlayerTracker.LateUpdate` 改为世界坐标浮点插值;`_currentRoom` 缓存避免每帧 GetRoom | ✅ 已修复 |
|
||
| P0 | `Canvas.ForceUpdateCanvases()` 全树重建 | 替换为 `LayoutRebuilder.ForceRebuildLayoutImmediate(_scrollRect.content)` | ✅ 已修复 |
|
||
| P1 | `MapPanel`/`MinimapHUD` 直接依赖 `MapPlayerTracker` 具体类 | 新建 `IPlayerPositionProvider` 接口;`MapPlayerTracker.Awake` 注册 ServiceLocator;Panel/HUD 改从 ServiceLocator 获取 | ✅ 已修复 |
|
||
| P1 | `MapPanel` 直接依赖 `MapPinManager` 具体类 | 新建 `IPinService` 接口;`MapPinManager.OnEnable/OnDisable` 注册/注销 ServiceLocator;`MapPanel` 改从 ServiceLocator 获取 | ✅ 已修复 |
|
||
| P1 | `RunValidation` 前缀误判 Bug(`err.Contains(r.RoomId)` 无边界) | 改为 `err.Contains($"'{r.RoomId}'")`,同时修复 `MapLayoutEditorWindow` 和 `MapDatabaseEditor` | ✅ 已修复 |
|
||
| P1 | `MapDatabaseSO._index` 编辑器修改后不清除 | `MapDatabaseSO` 新增 `private void OnValidate() => _index = null;` | ✅ 已修复 |
|
||
| P2 | `32f` 魔法数字分散三处 | 新建 `MapGridConstants.cs`(`FullMapCellPixels = 32f`);`MapRoomCellUI.Setup()` 接收 `float pixelsPerCell` 参数(默认值为常量);`MapPanel.DrawExits` 使用常量 | ✅ 已修复 |
|
||
| P2 | `GUIStyle` 每帧 `new`(编辑器 ~12000 次/秒分配) | `MapLayoutEditorWindow` 新增 3 个缓存字段(`_roomLabelStyle`/`_badgeBossStyle`/`_badgeNormalStyle`);`EnsureLabelStyles()` 仅在 `_zoom` 变化时重建 | ✅ 已修复 |
|
||
| P2 | `errorSet` 每帧在 `OnInspectorGUI` 重建 | `MapDatabaseEditor` 新增 `private readonly HashSet<string> _cachedErrorRoomIds`;仅在验证按钮点击时重建 | ✅ 已修复 |
|
||
| P2 | `GetComponent<RectTransform>()` 在热路径中未缓存(3 处) | `MapRoomCellUI` 新增 `public RectTransform RT { get; private set; }`(Awake 中赋值);`MapPanel` 全部改用 `cell.RT`;`MinimapHUD.PlaceCell` 改用 `cell.RT` | ✅ 已修复 |
|
||
| P3 | `RenderPins` 每次地图打开全量销毁重建,无脏检查 | 新增 `private int _lastPinVersion = -1`;`MapPinManager` 新增 `PinsVersion` 计数器(AddPin/RemovePin/OnLoad 时自增);`RenderPins` 跳过版本未变的重建 | ✅ 已修复 |
|
||
| P3 | `GetPinSprite` O(N) 线性扫描 | `MapPanel.Awake` 预构建 `Dictionary<PinType, Sprite> _pinSpriteDict`;`GetPinSprite` 改为 O(1) 查找 | ✅ 已修复 |
|
||
| P3 | `MinimapHUD._toRemove` 每帧 `new List<string>(8)` | 改为 `private readonly List<string> _toRemove = new List<string>(8)` 字段;RefreshView 头部调用 `.Clear()` 复用 | ✅ 已修复 |
|
||
| P3 | `PinSpriteEntry` 定义在 `MapPanel.cs` 位置不当 | 移入 `MapPin.cs`(与 `PinType`/`MapPinManager` 同文件,同 namespace,对调用方透明) | ✅ 已修复 |
|
||
|
||
### 新增文件清单
|
||
|
||
| 文件路径 | 说明 |
|
||
|---------|------|
|
||
| `Assets/_Game/Scripts/World/Map/MapGridConstants.cs` | 全局格子像素常量 `FullMapCellPixels = 32f` |
|
||
| `Assets/_Game/Scripts/World/Map/IPlayerPositionProvider.cs` | 玩家位置抽象接口 |
|
||
| `Assets/_Game/Scripts/World/Map/IPinService.cs` | 标记管理抽象接口 |
|
||
|
||
### 修复后架构图(实际状态)
|
||
|
||
```
|
||
IMapService ← ServiceLocator ← MapManager (ISaveable)
|
||
IPlayerPositionProvider ← ServiceLocator ← MapPlayerTracker
|
||
IPinService ← ServiceLocator ← MapPinManager (ISaveable)
|
||
|
||
MapPanel → ServiceLocator → IMapService / IPlayerPositionProvider / IPinService
|
||
MinimapHUD → ServiceLocator → IMapService / IPlayerPositionProvider
|
||
```
|
||
|
||
所有消费方(`MapPanel`、`MinimapHUD`)**零具体类 SerializeField 依赖**,完全通过接口和 ServiceLocator 通信。
|
||
|
||
|
||
系统经过两轮迭代已达到扎实基础:接口层设计良好,数据模型完整,编辑器工具远超行业平均水准。
|
||
但 89/100 的自评分高估了约 10 分,主要原因是:
|
||
|
||
1. **关键玩家体验缺陷被遗漏**(图标离散跳动)
|
||
2. **对"接口设计"维度宽松处理了两处直接具体类依赖**
|
||
3. **编辑器工具的性能问题(每帧 new GUIStyle,每帧重建 errorSet)未被察觉**
|
||
4. **有正确性 Bug 的 RunValidation 子字符串匹配被归类为纯性能问题**
|
||
|
||
修订后 **79/100** 仍代表一个结构清晰、工程质量优于多数独立游戏项目的地图系统,但距离"成熟专业"基准(以《空洞骑士》为标杆)尚有明确可落地的改进空间。
|