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.
This commit is contained in:
2026-05-25 23:15:12 +08:00
parent e2bc324905
commit f74d7f1877
53 changed files with 6825 additions and 270 deletions

View File

@@ -2,10 +2,28 @@ using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Serialization;
using BaseGames.Core.Events;
namespace BaseGames.World.Map
{
/// <summary>
/// 房间功能类型标记(可多选)。
/// 替代原先的 IsBossRoom / IsSavePoint / IsShop 三个独立 bool
/// 支持复合类型(如一个房间既是存档点也是商店),并便于扩展新类型。
/// </summary>
[System.Flags]
public enum RoomType
{
None = 0,
BossRoom = 1 << 0,
SavePoint = 1 << 1,
Shop = 1 << 2,
Merchant = 1 << 3,
Challenge = 1 << 4,
TeleportStation = 1 << 5,
}
/// <summary>
/// 单个房间的地图数据 SO架构 15_MapShopModule §1.1)。
/// 资产路径: Assets/_Game/Data/Map/Rooms/Room_{RoomId}.asset
@@ -28,11 +46,27 @@ namespace BaseGames.World.Map
[Header("出口信息")]
public RoomExitData[] Exits; // 该房间所有出口定义
[Header("特殊标记")]
public bool IsBossRoom;
public bool IsSavePoint;
public bool IsShop;
public Sprite MapIconOverride; // null = 按 isXxx 自动选择图标
[Header("房间类型标记(可多选)")]
public RoomType RoomFlags; // 支持多类型组合,替代旧的三个 bool 字段
[HideInInspector] public bool IsBossRoom; // 旧字段保留序列化兼容性OnValidate 自动迁移到 RoomFlags
[HideInInspector] public bool IsSavePoint; // 旧字段保留序列化兼容性OnValidate 自动迁移到 RoomFlags
[HideInInspector] public bool IsShop; // 旧字段保留序列化兼容性OnValidate 自动迁移到 RoomFlags
public Sprite MapIconOverride; // null = 按 RoomFlags 自动选择图标
/// <summary>
/// R20-N2 集中图标优先级逻辑,替代 MapPanel / MinimapHUD 各自重复的 ChooseIcon 实现。
/// 优先级MapIconOverride > SavePoint > BossRoom > Shop > TeleportStation。
/// 对应 Sprite 未配置时返回 null格子不显示图标
/// </summary>
public Sprite ChooseDisplayIcon(Sprite savePoint, Sprite boss, Sprite shop, Sprite teleport)
{
if (MapIconOverride != null) return MapIconOverride;
if (RoomFlags.HasFlag(RoomType.SavePoint) || IsSavePoint) return savePoint;
if (RoomFlags.HasFlag(RoomType.BossRoom) || IsBossRoom) return boss;
if (RoomFlags.HasFlag(RoomType.Shop) || IsShop) return shop;
if (RoomFlags.HasFlag(RoomType.TeleportStation)) return teleport;
return null;
}
[Header("流式加载")]
[Tooltip("此房间场景资产的预估内存KB。\n" +
@@ -45,7 +79,46 @@ namespace BaseGames.World.Map
{
// 保证 GridSize 每轴最小为 1防止零尺寸房间导致碰撞和渲染异常
GridSize = new Vector2Int(Mathf.Max(1, GridSize.x), Mathf.Max(1, GridSize.y));
// R11-N11 自动修剪 RoomId 首尾空格,避免 " Room_A " 与 "Room_A" 被视为不同键
if (!string.IsNullOrEmpty(RoomId) && RoomId != RoomId.Trim())
RoomId = RoomId.Trim();
// R12-N9 将旧 bool 字段迁移到 RoomFlagsRoomFlags 为 None 且旧字段有值时执行一次)
if (RoomFlags == RoomType.None)
{
if (IsBossRoom) RoomFlags |= RoomType.BossRoom;
if (IsSavePoint) RoomFlags |= RoomType.SavePoint;
if (IsShop) RoomFlags |= RoomType.Shop;
}
#if UNITY_EDITOR
// R11-N2 先 -= 再 +=,保证同一 delayCall 序列中最多执行一次,
// 防止 Inspector 快速拖动滑条时重复追加 N×FindAssets 导致卡顿
UnityEditor.EditorApplication.delayCall -= NotifyOwningDatabases;
UnityEditor.EditorApplication.delayCall += NotifyOwningDatabases;
#endif
}
#if UNITY_EDITOR
private void NotifyOwningDatabases()
{
if (this == null) return;
var guids = UnityEditor.AssetDatabase.FindAssets("t:MapDatabaseSO");
foreach (var guid in guids)
{
var path = UnityEditor.AssetDatabase.GUIDToAssetPath(guid);
var db = UnityEditor.AssetDatabase.LoadAssetAtPath<MapDatabaseSO>(path);
if (db?.AllRooms == null) continue;
if (System.Array.IndexOf(db.AllRooms, this) < 0) continue;
db.InvalidateIndex();
// Play Mode 下同时广播事件,让 UI 立即重建
if (UnityEngine.Application.isPlaying)
BaseGames.Core.ServiceLocator.GetOrDefault<IMapService>()?.NotifyDatabaseChanged();
}
}
#endif
}
[Serializable]
@@ -59,6 +132,10 @@ namespace BaseGames.World.Map
"Seamless无缝切换同区域相邻房间首选\n" +
"AtmosphericFade短暂淡出 + 区域名提示(跨大区域边界首选)。")]
public TransitionType PreferredTransitionType;
[Tooltip("是否已手动配置出口格子坐标。\n" +
"未勾选时,连线回退到房间中心,避免 (0,0) 与合法原点坐标产生歧义。")]
public bool HasCustomExitPos; // R12-N5 替代 ExitGridPos != Vector2Int.zero 哨兵用法
}
public enum ExitDirection { Up, Down, Left, Right }
@@ -72,9 +149,20 @@ namespace BaseGames.World.Map
[CreateAssetMenu(menuName = "BaseGames/World/Map/MapDatabase")]
public class MapDatabaseSO : ScriptableObject
{
public MapRoomDataSO[] AllRooms;
[SerializeField, FormerlySerializedAs("AllRooms")]
private MapRoomDataSO[] _allRooms;
private Dictionary<string, MapRoomDataSO> _index;
[SerializeField, Tooltip("勾选后AssetPostprocessor 自动注册的新房间会优先加入此 Database多个勾选时取 GUID 排序首个。")]
private bool _isDefault;
/// <summary>所属全部房间(只读视图)。编辑器写入请通过 <see cref="EditorSetRooms"/>。</summary>
public MapRoomDataSO[] AllRooms => _allRooms;
/// <summary>是否被标记为默认 Database。<see cref="MapRoomAutoRegister"/> 据此决定新建房间归属。</summary>
public bool IsDefault => _isDefault;
private Dictionary<string, MapRoomDataSO> _index;
private Dictionary<Vector2Int, string> _cellToRoom; // 格子坐标 → 房间 ID 空间索引(共享给 MinimapHUD/MapPlayerTracker避免重复构建
/// <summary>运行时快速查找(首次调用时建立索引)。</summary>
public MapRoomDataSO GetRoom(string roomId)
@@ -82,16 +170,77 @@ namespace BaseGames.World.Map
if (_index == null)
{
if (AllRooms == null) return null;
_index = AllRooms.Where(r => r != null)
.ToDictionary(r => r.RoomId);
// R29-N2 使用 TryAdd首条胜出防止重复 RoomId 触发 ArgumentException
// 编辑器侧 ValidateAll 负责提示策划修复数据,运行时继续工作不崩溃。
_index = new Dictionary<string, MapRoomDataSO>();
foreach (var r in AllRooms)
if (r != null && !string.IsNullOrEmpty(r.RoomId))
_index.TryAdd(r.RoomId, r);
}
_index.TryGetValue(roomId, out var r);
return r;
}
private void OnDisable() => _index = null; // SO 卸载时清理缓存
/// <summary>
/// 在指定格子坐标处查询所属房间 IDO(1) 哈希查找)。
/// 首次调用时惰性构建空间索引,由所有消费方(小地图/玩家追踪)共享,避免重复构建。
/// </summary>
public string GetRoomIdAtCell(Vector2Int cell)
{
EnsureSpatialIndex();
return _cellToRoom != null && _cellToRoom.TryGetValue(cell, out var id) ? id : null;
}
private void OnValidate() => _index = null; // 编辑器中修改 AllRooms 后强制重建索引
private void EnsureSpatialIndex()
{
if (_cellToRoom != null) return;
_cellToRoom = new Dictionary<Vector2Int, string>();
if (AllRooms == null) return;
foreach (var room in AllRooms)
{
if (room == null) continue;
for (int x = 0; x < room.GridSize.x; x++)
for (int y = 0; y < room.GridSize.y; y++)
_cellToRoom[new Vector2Int(room.GridPosition.x + x, room.GridPosition.y + y)] = room.RoomId;
}
}
/// <summary>数据库变更时(编辑器热改 / 运行时热更)强制让索引下次访问时重建。</summary>
public void InvalidateIndex()
{
_index = null;
_cellToRoom = null;
}
#if UNITY_EDITOR
/// <summary>
/// 编辑器专用:写入 <see cref="AllRooms"/> 数组并强制失效空间索引。
/// 替代直接赋值 public 字段,确保 <see cref="MapRoomAutoRegister"/> / 测试代码不绕过封装。
/// </summary>
public void EditorSetRooms(MapRoomDataSO[] rooms)
{
_allRooms = rooms;
InvalidateIndex();
}
#endif
private void OnDisable()
{
// SO 卸载时清理缓存
_index = null;
_cellToRoom = null;
}
private void OnValidate()
{
// 编辑器中修改 AllRooms 后强制重建索引
_index = null;
_cellToRoom = null;
#if UNITY_EDITOR
if (UnityEngine.Application.isPlaying)
BaseGames.Core.ServiceLocator.GetOrDefault<IMapService>()?.NotifyDatabaseChanged();
#endif
}
// ── 配置验证 ──────────────────────────────────────────────────────────
#if UNITY_EDITOR
@@ -111,6 +260,15 @@ namespace BaseGames.World.Map
if (AllRooms[i] == null) { errors.Add($"AllRooms[{i}] 为 null"); continue; }
if (string.IsNullOrEmpty(AllRooms[i].RoomId))
errors.Add($"AllRooms[{i}]{AllRooms[i].name}RoomId 为空");
else
{
// R11-N11 首尾空格检查OnValidate 已自动 Trim此处兜底提示未经 OnValidate 的旧资产)
if (AllRooms[i].RoomId != AllRooms[i].RoomId.Trim())
errors.Add($"'{AllRooms[i].name}' RoomId 含首尾空格,请在 Inspector 中保存触发自动修剪");
// 特殊字符检查:/ \ | 等可能影响路径/键处理的字符
if (AllRooms[i].RoomId.IndexOfAny(new[]{ '/', '\\', '|', '<', '>', '*', '?' }) >= 0)
errors.Add($"'{AllRooms[i].RoomId}' 含非法字符(/ \\ | < > * ?),可能影响场景名匹配和存档键");
}
}
// ② RoomId 重复