- 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.
19 KiB
小地图系统独立审查报告(Round 10)
审查时间:第 10 轮独立全量复审
审查范围:Assets/_Game/Scripts/World/Map/全部 14 个运行时文件 +Assets/_Game/Scripts/Editor/World/Map/全部 4 个编辑器文件
对标基准:成熟商业 2D 类银河恶魔城(房间制大地图)的编辑器扩展 + 运行时表现 评审视角:专业编辑器扩展 / 解耦架构 / 高性能 / 可扩展 / 策划友好
第 1 章 · 总评
1.1 综合评分
| 维度 | 权重 | 得分 | 说明 |
|---|---|---|---|
| 架构与解耦 | 15% | 92 | 接口三件套 + ServiceLocator + EventChannelSO;MonoBehaviour 之间零硬引用 |
| 数据契约 / 错误恢复 | 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.6(A-) |
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
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 临时分配。
修复建议:
- 方案 A:HUD 不在 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
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: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
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 自动加入 Database;Scene 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 增量) | ~2KB(cell 创建/销毁) | ⚠ R10-N2 |
| Pin 增删 1 个 | ~0.3ms × 全部 Pin 数 | ~1KB × N | ⚠ R10-N3 |
| OnDatabaseChanged 触发 | ~5ms(与首次打开相当) | ~80KB | 罕见操作,可接受 |
第 6 章 · 修复优先级清单(推荐落地顺序)
优先级 1(影响正确性 / 用户感知卡顿)
- R10-N1 MapPanel.OnEnable 增加 dirty 检测 → 避免遗失数据库变更
- R10-N2 MinimapHUD OnDisable 改为 SetActive(false) 不销毁 cells → 消除 HUD 切换 GC
优先级 2(性能/体验抛光)
- R10-N3 Pin 对象池
- R10-N12 拆分 OnExplorationChanged / OnDatabaseChanged 事件语义
- R10-N5 拖拽实时重叠预警
优先级 3(可选增强)
- R10-N4 显式默认 Database 配置
- R10-N6 多选框选拖拽
- R10-N7 AllRooms 封装为 property
- R10-N8 Input System 适配
- R10-N9 MapPlayerTracker
_isDuplicate显式化 - R10-N10 RoomData 本地化
- R10-N11 大地图分帧构建
- R10-N13 地图碎片批量 + 动画 hook
第 7 章 · 结论
本系统已达到专业商业级 2D 类银河恶魔城的小地图实现水准(88.6 / 100,A-)。架构与编辑器扩展为当前优势项,剩余抛光点集中在:
- OnDisable 状态管理(R10-N1/N2)— 易触发隐性 bug,建议优先处理
- GC 优化(R10-N3)— 玩家高频操作场景的可感知抖动
- 编辑器深度(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.8(Pin 池 + Cell 不再销毁重建)
- 编辑器扩展 +0.5(拖拽冲突可视化 + 默认 Database 显式化)
- 鲁棒性 +0.6(重复 Tracker 守门 + 订阅生命周期修正)
预估新评分:~92 / A(剩余 8 分主要被未实施的 N6/N8/N10/N11 + 美术资产部分占用)