- 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.
21 KiB
小地图系统 Round 11 独立评估报告
评估时间:2026-05-25
基准版本:R10 全部修复落地后当前 HEAD
评估范围:Assets/_Game/Scripts/World/Map/+Assets/_Game/Scripts/Editor/World/Map/
对标标准:成熟 2D 银河恶魔城游戏标准(高性能、高解耦、策划友好、编辑器一流)
第 1 章:整体评分
| 维度 | 分值(满 10) | 较 R10 |
|---|---|---|
| 架构解耦 | 8.5 | ↑0.5(事件语义分离完成) |
| 数据设计 | 8.5 | ↑0(稳定) |
| 运行时性能 | 8.5 | ↑0.3(Pin 池 + Cell 保留落地) |
| 编辑器扩展 | 8.0 | ↑0.5(拖拽冲突可视化、IsDefault) |
| 策划友好性 | 7.5 | ↑0(仍缺 DisplayName 本地化) |
| 功能完整性 | 8.5 | ↑0(稳定) |
| 鲁棒性 | 7.5 | ↓0.5(发现 N11 部分订阅 Bug) |
| 可扩展性 | 8.5 | ↑0.3(SetMappedBatch、OnRoomMapped) |
加权综合得分:85.6 / 100(B+)
注:R10 修复整体质量优秀;本轮发现 N1 MinimapHUD 部分订阅 Bug(P1 级别真实缺陷),导致鲁棒性维度扣分,综合分低于 R10 预估的 92 分。
第 2 章:系统亮点
2.1 接口与事件设计(9/10)
IMapService完整定义了三个语义明确的事件:OnDatabaseChanged(结构变更)/OnExplorationChanged(探索进度)/OnRoomMapped(单房间解锁)。- 消费方(MapPanel、MinimapHUD)通过接口与 ServiceLocator 完全解耦,不持有任何具体 MonoBehaviour 引用。
MapServiceExtensions.GetVisibility集中三级可见性推导逻辑,避免分散重复。
2.2 空间索引共享(9/10)
MapDatabaseSO.GetRoomIdAtCell(Vector2Int)惰性构建一次,供MapPlayerTracker/MinimapHUD.RefreshView共享,O(1) 格子查找。InvalidateIndex在结构变更时统一失效,不存在缓存过期风险。
2.3 MinimapHUD 增量刷新(8.5/10)
RefreshView为 O(viewRadius²) 而非 O(allRooms),大地图下效果显著。- 回收/新建格子避免全量重建,
_toRemove/_roomsInViewBuffer列表复用消除高频 GC。
2.4 编辑器工具套件(8/10)
MapLayoutEditorWindow:格子布局预览 + 区域着色 + 拖拽移房 + 冲突可视化(R10-N5)+ 搜索/图例 + 验证 + Play Mode 玩家位置。MapRoomDataEditor:Scene View 双角控制点直接拖拽,策划可在场景中直观编辑房间尺寸。MapRoomAutoRegister:新建 SO 自动追加到默认 Database,消灭策划忘记注册的问题。
2.5 数据兼容性保障(9/10)
[FormerlySerializedAs("AllRooms")]确保_allRooms字段重命名后现有.asset不丢失数据。EditorSetRooms专用写入器防止外部代码绕过封装直接赋值。
第 3 章:新发现问题(R11-N1 ~ N12)
R11-N1 ★P1★ — MinimapHUD _subscribed 标志导致部分订阅场景下事件永不触发
文件: MinimapHUD.cs → SubscribeServices()
现象:
private void SubscribeServices()
{
_mapSvc ??= ServiceLocator.GetOrDefault<IMapService>();
_playerProvider ??= ServiceLocator.GetOrDefault<IPlayerPositionProvider>();
_pinService ??= ServiceLocator.GetOrDefault<IPinService>();
if (_subscribed) return; // ← 提前 return 阻断后续
if (_mapSvc == null && _playerProvider == null) return;
if (_playerProvider != null)
_playerProvider.OnRoomChanged += OnRoomChanged;
if (_mapSvc != null)
{
_mapSvc.OnDatabaseChanged += OnDatabaseChanged;
_mapSvc.OnExplorationChanged += OnExplorationChanged;
}
_subscribed = true; // ← 仅当上方至少一个服务非 null 时才置位
}
具体 Bug:
场景——_playerProvider 在 Awake 时已注册(优先 ExecutionOrder),但 _mapSvc 尚未就绪:
- 第一次调用:
_playerProvider成功,_mapSvc == null→ 仅订阅OnRoomChanged,置_subscribed = true。 - 后续调用:
_mapSvc现已就绪,但if (_subscribed) return提前退出,OnDatabaseChanged/OnExplorationChanged永远不订阅。 - 结果:小地图 HUD 读档后不刷新、房间解锁后不更新颜色。
修复方案: 改为分别追踪 _mapSvcSubscribed / _playerSubscribed,或直接仿照 MapPanel 的模式(每个服务独立 if (svc == null) 守门)。
R11-N2 ★P1★ — MapRoomDataSO.OnValidate 重复向 delayCall 追加委托
文件: MapRoomDataSO.cs → OnValidate()
private void OnValidate()
{
GridSize = new Vector2Int(Mathf.Max(1, GridSize.x), Mathf.Max(1, GridSize.y));
#if UNITY_EDITOR
UnityEditor.EditorApplication.delayCall += NotifyOwningDatabases; // ← 问题所在
#endif
}
问题: delayCall 是多播委托(+=)。当策划在 Inspector 中快速拖动滑条时,OnValidate 每帧调用一次,NotifyOwningDatabases 被追加数十次。该方法内部执行 FindAssets + LoadAssetAtPath(昂贵),会在下一帧批量执行导致卡顿。
修复方案: 先 -= 再 +=,保证同一 delayCall 序列中最多一次:
EditorApplication.delayCall -= NotifyOwningDatabases;
EditorApplication.delayCall += NotifyOwningDatabases;
R11-N3 ★P1★ — MapPinManager.OnLoad 直接赋值反序列化 List,共享 SaveData 引用
文件: MapPin.cs → MapPinManager.OnLoad()
public void OnLoad(SaveData data)
{
_pins = data.Map.Pins ?? new List<MapPin>(); // ← 直接赋值,不是拷贝
PinsVersion++;
}
OnSave 做了防御性拷贝(new List<MapPin>(_pins)),但 OnLoad 反方向没有拷贝。若调用方在 OnLoad 后继续持有 data 并修改 data.Map.Pins,会污染 _pins。
修复:
_pins = data.Map.Pins != null ? new List<MapPin>(data.Map.Pins) : new List<MapPin>();
R11-N4 ★P1★ — MapPanel.CenterOnCurrentRoom 对整个 content 节点调用 ForceRebuildLayoutImmediate
文件: MapPanel.cs → CenterOnCurrentRoom()
LayoutRebuilder.ForceRebuildLayoutImmediate(_scrollRect.content);
ForceRebuildLayoutImmediate 会递归重建参数节点及其所有子节点的 Layout。_scrollRect.content 下有所有 MapRoomCellUI 实例,重建代价随房间数线性增长(N 房间 = N 次 layout 计算)。面板每次 OnEnable 时执行一次,常规用法中可接受;但若项目规模扩展到 200+ 房间,此处会成为明显延迟点。
建议: 只有在 content 布局确实发生变化时(BuildGrid 之后)才 ForceRebuild;若 ScrollRect 没有使用 LayoutGroup,可改为直接计算 normalizedPosition,完全跳过 ForceRebuildLayoutImmediate。
R11-N5 ★P2★ — MinimapHUD 对 MapRoomCellUI 无对象池,跨房间时 GC 抖动
文件: MinimapHUD.cs → RefreshView() → cell 回收段
if (cell != null) Destroy(cell.gameObject); // ← 销毁而非入池
MinimapHUD 的 RefreshView 在玩家跨越房间边界时,会销毁视野外的 MapRoomCellUI GameObject 并重新实例化新进入视野的格子。
- 典型场景(走廊穿梭):每次房间切换约销毁/创建 3-8 个 Cell GameObject,频率可达 1-2 次/秒。
- Pin 已有对象池,但 Cell 没有,导致一定 GC 压力。
建议: 为 MapRoomCellUI 建立 Stack<MapRoomCellUI> _cellPool,回收时 SetActive(false) 入池,需要时出池重置,与 Pin 池保持一致。
R11-N6 ★P2★ — MapManager.GetRoomsByRegion 每次调用都分配新数组
文件: MapManager.cs
public MapRoomDataSO[] GetRoomsByRegion(string regionId)
=> _database.AllRooms.Where(r => r != null && r.RegionId == regionId).ToArray();
每次调用分配 LINQ 枚举器 + 结果数组。若调用方(如 MapPanel 地区筛选、成就系统)在 Update 中使用,会造成 GC。
建议: 加结果缓存(Dictionary<string, MapRoomDataSO[]>),在 NotifyDatabaseChanged 时失效。
R11-N7 ★P2★ — MapLayoutEditorWindow 不监听外部资产变更
文件: MapLayoutEditorWindow.cs
窗口打开后:
- 若从代码/其他窗口修改
MapDatabaseSO(如 MapDatabaseEditor 的 Validate 按钮),布局窗口不自动刷新,需用户手动交互。 Undo.undoRedoPerformed正确注册,但外部变更(EditorUtility.SetDirty后保存、脚本修改资产)不触发Repaint。
建议: 监听 EditorApplication.projectWindowItemOnGUI 或使用 AssetDatabase.postprocessAllAssets;或在 OnGUI 开头检查 database 的 AllRooms 数组引用变化(版本号方案)。
R11-N8 ★P2★ — MapRoomCellUI.Setup 的 pixelsPerCell 参数对 MinimapHUD 调用路径存在 API 语义歧义
文件: MapRoomCellUI.cs / MinimapHUD.cs
// MinimapHUD 调用路径:
cell.Setup(room, _mapSvc.GetVisibility(room.RoomId), null); // 使用默认 pixelsPerCell=32
cell.SetColors(_colorExplored, _colorMapped, _colorUnknown);
PlaceCell(cell, room); // 立即覆盖 RT.anchoredPosition 和 sizeDelta
Setup 内部已根据 pixelsPerCell 计算并写入了 RT.anchoredPosition / RT.sizeDelta,但 MinimapHUD 立即用 PlaceCell 覆盖,造成无意义的写入。pixelsPerCell 参数对 MinimapHUD 路径无实际效果,但 API 签名暗示它有意义,容易误导维护者。
建议: 在 Setup 中将位置/尺寸计算提取为 SetGridLayout(room, pixelsPerCell) 方法,MinimapHUD 调用 Setup 时不传位置参数,由 PlaceCell 统一负责布局。或简化为重载:Setup(room, visibility, icon) + Setup(room, visibility, icon, pixelsPerCell)。
R11-N9 ★P2★ — MapPlayerTracker 假设世界原点与格子原点重合,无 WorldOffset 参数
文件: MapPlayerTracker.cs
private Vector2Int WorldToCell(Vector2 worldPos)
=> new(Mathf.FloorToInt(worldPos.x / _worldUnitsPerCell),
Mathf.FloorToInt(worldPos.y / _worldUnitsPerCell));
如果关卡世界坐标原点不在 (0,0)(如整个世界在 Y=-500 以下),此计算会得到错误的格子坐标,导致玩家位置追踪完全失效。
建议: 增加 [SerializeField] private Vector2 _worldOriginOffset 字段,WorldToCell 先减去 _worldOriginOffset 再除以 _worldUnitsPerCell。
R11-N10 ★P2★ — MapLayoutEditorWindow.DrawExitLines 连线使用房间中心而非实际出口格子坐标
文件: MapLayoutEditorWindow.cs → DrawExitLines()
Vector2 from = GridCenterToClip(room.GridPosition + room.GridSize / 2, origin); // 房间中心
Vector2 to = GridCenterToClip(target.GridPosition + target.GridSize / 2, origin);
RoomExitData 结构中已有 ExitGridPos 字段(出口在格子地图上的实际位置),但 DrawExitLines 画的是两个房间中心之间的连线。对于大尺寸房间,连线起止点可能距离实际出口较远,策划无法直观判断出口对齐情况。
建议: 改为从 exit.ExitGridPos 到对应 target 房间的对应出口格子坐标,若 target 无对应出口则退化为中心连线。
R11-N11 ★P2★ — MapRoomDataSO 公共字段缺少 RoomId 命名规则验证
文件: MapRoomDataSO.cs / MapDatabaseSO.cs → ValidateAll()
RoomId 字段直接用于:
- 场景名匹配(
OnRoomEntered事件传入场景名) - Dictionary key 查找
- 存档 HashSet 存储
目前 ValidateAll 检查了重复和空值,但未检查:
- 首尾空格(
" Room_A "与"Room_A"被视为不同但功能等效时易混淆) - 特殊字符(
/、\等可能影响路径处理的字符)
建议: 在 MapRoomDataSO.OnValidate 中自动 Trim();在 ValidateAll 中增加含空格/特殊字符的警告。
R11-N12 ★P3★ — MapLayoutEditorWindow.DrawExitLines 连线在极端缩放(≤ 0°)时 GUI.matrix 未正确恢复
文件: MapLayoutEditorWindow.cs → DrawLine()
GUIUtility.RotateAroundPivot(angle, mid);
GUI.DrawTexture(...);
GUI.matrix = prevMatrix; // 手动恢复
若 DrawTexture 抛出异常(如纹理被意外卸载),GUI.matrix 不会被恢复,导致整个窗口绘制出现旋转偏移。
建议: 使用 using (new GUIMatrixScope(...)) 或 try/finally 包裹:
var prev = GUI.matrix;
try { GUIUtility.RotateAroundPivot(angle, mid); GUI.DrawTexture(...); }
finally { GUI.matrix = prev; }
第 4 章:维度详细评分
4.1 架构解耦 — 8.5/10
优秀:
- IMapService 接口三个独立事件,语义清晰
- ServiceLocator 注册在 Awake/OnDestroy,生命周期正确
- MapPinManager 独立于 MapManager,通过 IPinService 解耦
- MapServiceExtensions 扩展方法集中可复用逻辑
不足:
- MinimapHUD
_subscribed标志存在部分订阅 Bug(N1 P1) - MapPanel 仍通过
StringEventChannelSO _onMapUpdated双通道接收单房间更新(OnMapUpdated + OnExplorationChanged 语义重叠但各有其用,轻微冗余)
4.2 数据设计 — 8.5/10
优秀:
- 三级可见性(Unknown / Explored / Mapped)精确匹配银河恶魔城标准
- MapDatabaseSO 懒构建双索引(id → data,cell → roomId),共享给所有消费方
- MapRoomDataSO.OnValidate 自动修正 GridSize 最小值
- RoomExitData 包含 TransitionType,为场景切换类型扩展预留
不足:
- GetRoomsByRegion 无结果缓存(N6 P2)
- DisplayName 无 i18n 路径(延续 R10-N10)
- RoomId 无命名规则强制检查(N11 P2)
4.3 运行时性能 — 8.5/10
优秀:
- MinimapHUD RefreshView O(viewRadius²),大地图下远快于 O(N)
- Pin 对象池(MapPanel + MinimapHUD),ClearPins = SetActive(false) 而非 Destroy
- 脏标志驱动 UI(databaseDirty / explorationDirty / viewDirty)
- LateUpdate 双重脏检查(PinsVersion + 玩家位置)
- MapDatabaseSO 空间索引 O(1) 哈希查找
不足:
- MinimapHUD MapRoomCellUI 无对象池(N5 P2),跨房间边界 GC 抖动
- CenterOnCurrentRoom 对 content ForceRebuildLayoutImmediate(N4 P1),大房间数时开销可见
- GetRoomsByRegion LINQ.ToArray() 无缓存(N6 P2)
4.4 编辑器扩展 — 8.0/10
优秀:
- MapLayoutEditorWindow 全功能:zoom/pan/drag/conflict/search/legend/validate/PlayMode 玩家点
- MapRoomDataEditor Scene View 双角控制点 + 吸附 + Undo + 居中快捷键
- MapDatabaseEditor 一键验证 + 房间列表 + 错误行红色高亮
- MapRoomAutoRegister 自动注册消除遗漏风险 + EditorPrefs 开关
- Undo/Redo 刷新支持
不足:
- 外部资产变更不触发窗口刷新(N7 P2)
- DrawExitLines 用中心连线而非实际出口格坐标(N10 P2)
- OnValidate delayCall 重复追加(N2 P1)
4.5 策划友好性 — 7.5/10
优秀:
- 布局编辑器拖拽房间 + 冲突立即变红,无需专业编程知识
- 搜索 + 图例 + 区域着色帮助大地图快速定位
- 自动注册新房间无需手动维护 Database
- Play Mode 实时玩家位置可视化
不足:
- 出口连线视觉不够精确(N10),策划无法确认出口对齐
- 无键盘快捷键(如 V = 验证,F = 重置视图)
- 无批量移动/对齐多个房间能力(延续 R10-N6)
- DisplayName 无法本地化预览(延续 R10-N10)
4.6 功能完整性 — 8.5/10
优秀:
- 全屏地图 + 角落小地图双 UI,与头部游戏相同配置
- SetMappedBatch 支持地图碎片批量解锁
- OnRoomMapped + OnRoomMappedAnim 虚钩子,解锁动画预留
- 探索进度 API(GetExplorationProgress / ExploredRoomCount)
- MapExplorationCondition 接入成就系统
- 三种特殊房间标记(Boss / SavePoint / Shop)+ MapIconOverride 自定义
- 房间出口数据 + 过渡类型
不足:
- 无"全地图揭示"调试命令(开发阶段常用)
- 无地图房间分组/层级(如地下层 / 地面层分图)
- RoomOutlineTex 非矩形形状支持存在(字段已有),但编辑器无预览
4.7 鲁棒性 — 7.5/10
优秀:
- MapManager / MapPlayerTracker 重复实例 Awake 检测 + _isDuplicate 守门
- 全量 null 守卫(空 Database / 空房间数组)
- FormerlySerializedAs 数据兼容
- ValidateAll 四类错误检测(null / 重复 ID / 格子重叠 / 出口悬空)
- _exploredRooms / _mappedRooms 使用 HashSet 防重复
严重不足:
- MinimapHUD 部分订阅 Bug(N1 P1):
OnDatabaseChanged/OnExplorationChanged在特定启动顺序下永不触发 - MapPinManager.OnLoad 共享 SaveData 列表引用(N3 P1)
4.8 可扩展性 — 8.5/10
优秀:
- IMapService 接口易于 Mock/测试替换
- SetMappedBatch + OnRoomMapped 为地图碎片系统提供一流扩展点
- protected virtual OnRoomMappedAnim 供 UI 子类实现动画
- RoomExitData.PreferredTransitionType 枚举为未来过渡系统预留
- MapServiceExtensions 扩展方法模式
- IsDefault 标志 + AutoRegister 支持多 Database 项目
不足:
- 无
IMapService.GetAllMappedRooms()/GetAllExploredRooms()返回快照 API(存档分析/成就系统需多次 HashSet 枚举) - MapRoomDataSO 无版本号字段(热更/DLC 房间 ID 变更无法追踪遗留数据)
第 5 章:优先级修复清单
P1 — 必须修复(影响正确性)
| 编号 | 位置 | 问题摘要 | 预估工时 |
|---|---|---|---|
| R11-N1 | MinimapHUD.SubscribeServices |
_subscribed 阻止部分订阅后续补全 → mapSvc 事件永不触发 |
1h |
| R11-N2 | MapRoomDataSO.OnValidate |
delayCall += 重复追加 → 批量编辑时 N×FindAssets 卡顿 |
0.5h |
| R11-N3 | MapPinManager.OnLoad |
直接赋值反序列化 List → 引用共享污染 SaveData | 0.5h |
| R11-N4 | MapPanel.CenterOnCurrentRoom |
ForceRebuildLayoutImmediate(content) → 大房间数时 OnEnable 卡顿 |
1h |
P2 — 应当修复(影响体验/维护)
| 编号 | 位置 | 问题摘要 |
|---|---|---|
| R11-N5 | MinimapHUD.RefreshView |
MapRoomCellUI 无对象池,跨房间 GC 抖动 |
| R11-N6 | MapManager.GetRoomsByRegion |
LINQ ToArray() 无缓存 |
| R11-N7 | MapLayoutEditorWindow |
外部资产变更不触发 Repaint |
| R11-N8 | MapRoomCellUI.Setup |
pixelsPerCell 参数对 MinimapHUD 路径无意义,API 歧义 |
| R11-N9 | MapPlayerTracker |
无 WorldOriginOffset,世界坐标偏移场景无法使用 |
| R11-N10 | MapLayoutEditorWindow.DrawExitLines |
中心连线而非出口格坐标,视觉精度低 |
| R11-N11 | MapRoomDataSO.OnValidate + ValidateAll |
RoomId 无命名规则检查(Trim / 空格 / 特殊字符) |
P3 — 可选优化
| 编号 | 问题摘要 |
|---|---|
| R11-N12 | DrawLine GUI.matrix 未在异常路径下恢复 |
第 6 章:与标杆游戏对比
| 特性 | 本系统 | 业界标杆 |
|---|---|---|
| 三级可见性 | ✅ Unknown / Explored / Mapped | ✅ 标准配置 |
| 角落小地图 | ✅ 视野半径可配置,增量刷新 | ✅ |
| 全屏地图 + ScrollRect 居中 | ✅ | ✅ |
| 地图碎片批量解锁 | ✅ SetMappedBatch | ✅(商店购买/触碰标牌解锁) |
| 地图标记(Pin)系统 | ✅ 多类型 + 存档 | ✅ |
| 非矩形房间形状 | ⚠️ 字段预留,编辑器无预览 | ✅(精细多边形遮罩) |
| 多区域地图(分图) | ❌ | ✅(地下/地表/秘境分区) |
| 房间Tooltip/命名 | ✅ DisplayName | ✅(带区域名动画) |
| 键盘导航地图 | ✅(WASD/方向键) | ✅ |
| 出口连接可视化 | ⚠️ 编辑器中心连线,运行时无连线 | ✅(点状通道指示) |
| 地图缩放(运行时) | ✅ 滚轮缩放 | ✅ |
| 地图揭示动画 | ⚠️ 钩子已预留,动画未实现 | ✅(逐格展开) |
第 7 章:总结
本系统在 R10 修复落地后已达到商业级银河恶魔城地图系统的主体功能,架构理念(接口 + ServiceLocator + 事件分离 + 脏标志)、编辑器工具套件(三窗口协同 + 自动注册)处于同类独立游戏工具的前列水平。
本轮发现的最高优先级问题集中在鲁棒性细节(MinimapHUD 部分订阅 Bug、OnValidate delayCall 堆积)和API 设计细节(Setup 参数歧义、GetRoomsByRegion 分配),修复这些问题后综合评分预估可恢复至 90~91 / 100(A-)。
长期来看,补齐以下能力可冲击 95/100(A):
- MapRoomCellUI 对象池(N5)
- 多区域/分图支持
- 非矩形房间轮廓编辑器预览
- 出口精确连线可视化(N10)
- WorldOriginOffset 参数(N9)
本报告独立于前序轮次评审,基于 2026-05-25 当前代码库完整重读后生成。