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:
@@ -3,6 +3,7 @@ using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Core.Save;
|
||||
|
||||
namespace BaseGames.World.Map
|
||||
{
|
||||
@@ -32,20 +33,44 @@ namespace BaseGames.World.Map
|
||||
[SerializeField] private Color _colorUnknown = Color.black;
|
||||
|
||||
[Header("Event Channels")]
|
||||
[SerializeField] private StringEventChannelSO _onMapUpdated; // 房间发现/标注时局部刷新
|
||||
// R18-N3 _onMapUpdated 订阅已废弃(对齐 MapPanel R12-N8):
|
||||
// OnExplorationChanged 全量刷新完全覆盖 OnMapUpdated 的单格更新,订阅形成冗余双重刷新。
|
||||
// 保留 [HideInInspector, SerializeField] 维持 Prefab 序列化兼容,不再订阅事件。
|
||||
[HideInInspector, SerializeField] private StringEventChannelSO _onMapUpdated;
|
||||
|
||||
[Header("地图标记(可选)")]
|
||||
[SerializeField] private Image _pinPrefab; // 留空则不渲染 Pin
|
||||
[SerializeField] private MapPinConfigSO _pinConfig; // R12-N3 集中 PinType→Sprite 映射
|
||||
|
||||
[Header("房间类型图标(可选,与全屏地图保持视觉一致)")]
|
||||
[SerializeField] private Sprite _iconSavePoint;
|
||||
[SerializeField] private Sprite _iconBossRoom;
|
||||
[SerializeField] private Sprite _iconShop;
|
||||
[Tooltip("传送站图标;房间含 TeleportStation 标志时显示。")]
|
||||
[SerializeField] private Sprite _iconTeleport;
|
||||
|
||||
[Header("缩放档位(可选)")]
|
||||
[SerializeField] private int[] _zoomLevels = { 2, 3, 5 }; // R12-FA 可用视野半径档位(格)
|
||||
private int _zoomLevelIndex;
|
||||
|
||||
private IMapService _mapSvc;
|
||||
private IPlayerPositionProvider _playerProvider;
|
||||
private IPinService _pinService;
|
||||
private readonly Dictionary<string, MapRoomCellUI> _cells = new();
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
private readonly List<Image> _pinImages = new();
|
||||
private readonly Stack<Image> _pinPool = new(); // R10-N3 Pin 对象池
|
||||
private readonly Stack<MapRoomCellUI> _cellPool = new(); // R11-N5 Cell 对象池
|
||||
private int _lastPinVersion = -1;
|
||||
private bool _viewDirty; // R10-N2 关闭期间收到 OnRoomChanged → 下次 OnEnable RefreshView
|
||||
private bool _databaseDirty; // R10-N1 关闭期间收到 OnDatabaseChanged → 下次 OnEnable 完整重建
|
||||
private bool _servicesReady; // R21-N1 三服务全部就绪后置 true,短路 LateUpdate 的每帧 ServiceLocator 查询(对齐 MapPanel)
|
||||
|
||||
// 复用 List 避免 RefreshView 每次分配临时 List(GC 友好)
|
||||
private readonly List<string> _toRemove = new List<string>(8);
|
||||
|
||||
// 空间索引:格子坐标 → 房间 ID,将 RefreshView step② 的 O(N) 遍历降至 O(viewRadius²)
|
||||
private Dictionary<Vector2Int, string> _spatialIndex;
|
||||
// 复用 HashSet 避免 RefreshView 每次分配(GC 友好)
|
||||
private readonly HashSet<string> _roomsInViewBuffer = new HashSet<string>(32);
|
||||
private readonly HashSet<string> _newlyAddedBuffer = new HashSet<string>(16);
|
||||
|
||||
private Vector2Int _currentCenter;
|
||||
private string _lastDotRoomId;
|
||||
@@ -53,73 +78,195 @@ namespace BaseGames.World.Map
|
||||
|
||||
// ── 生命周期 ──────────────────────────────────────────────────────────
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// R10-N2/N1 服务订阅在 Awake/OnDestroy 长期持有,OnDisable 不解绑
|
||||
// 即便 HUD 隐藏期间发生 OnRoomChanged / OnDatabaseChanged,也能记录 dirty 标志
|
||||
SubscribeServices();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
_mapSvc = ServiceLocator.GetOrDefault<IMapService>();
|
||||
_playerProvider = ServiceLocator.GetOrDefault<IPlayerPositionProvider>();
|
||||
// 启动顺序兜底
|
||||
SubscribeServices();
|
||||
|
||||
BuildSpatialIndex(_mapSvc?.Database);
|
||||
// R10-N1/N2 应用关闭期间累积的状态变化
|
||||
if (_databaseDirty)
|
||||
{
|
||||
ClearAllCells();
|
||||
ClearPins();
|
||||
_lastPinVersion = -1;
|
||||
_databaseDirty = false;
|
||||
_viewDirty = true; // 重建后需 RefreshView
|
||||
}
|
||||
|
||||
if (_playerProvider != null)
|
||||
_playerProvider.OnRoomChanged += OnRoomChanged;
|
||||
|
||||
_onMapUpdated?.Subscribe(OnMapUpdated).AddTo(_subs);
|
||||
|
||||
// 首次显示时立即刷新
|
||||
RefreshView();
|
||||
if (_viewDirty || _cells.Count == 0)
|
||||
{
|
||||
_viewDirty = false;
|
||||
RefreshView();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (_playerProvider != null)
|
||||
_playerProvider.OnRoomChanged -= OnRoomChanged;
|
||||
|
||||
_subs.Clear();
|
||||
ClearAllCells();
|
||||
_lastDotRoomId = null;
|
||||
_mapSvc = null;
|
||||
_playerProvider = null;
|
||||
_spatialIndex = null;
|
||||
// R10-N2 保留 cells/pins/服务订阅;HUD 频繁开关时避免 GC 抖动
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
UnsubscribeServices();
|
||||
ClearAllCells();
|
||||
ClearPins();
|
||||
foreach (var img in _pinPool)
|
||||
if (img != null) Destroy(img.gameObject);
|
||||
_pinPool.Clear();
|
||||
foreach (var cell in _cellPool)
|
||||
if (cell != null) Destroy(cell.gameObject);
|
||||
_cellPool.Clear();
|
||||
}
|
||||
|
||||
private void SubscribeServices()
|
||||
{
|
||||
// 各服务独立守门:任意服务迟到就绪时,后续调用仍能补订阅(R11-N1 修复)
|
||||
if (_playerProvider == null)
|
||||
{
|
||||
_playerProvider = ServiceLocator.GetOrDefault<IPlayerPositionProvider>();
|
||||
if (_playerProvider != null)
|
||||
_playerProvider.OnRoomChanged += OnRoomChanged;
|
||||
}
|
||||
if (_mapSvc == null)
|
||||
{
|
||||
_mapSvc = ServiceLocator.GetOrDefault<IMapService>();
|
||||
if (_mapSvc != null)
|
||||
{
|
||||
_mapSvc.OnDatabaseChanged += OnDatabaseChanged;
|
||||
_mapSvc.OnExplorationChanged += OnExplorationChanged;
|
||||
}
|
||||
}
|
||||
_pinService ??= ServiceLocator.GetOrDefault<IPinService>();
|
||||
|
||||
// R21-N1 三服务全部就绪后置 true,短路 LateUpdate 的每帧查询(对齐 MapPanel._servicesReady)
|
||||
if (_playerProvider != null && _mapSvc != null && _pinService != null)
|
||||
_servicesReady = true;
|
||||
}
|
||||
|
||||
private void UnsubscribeServices()
|
||||
{
|
||||
_servicesReady = false; // R21-N3 重置,确保重建场景后的实例能重新订阅
|
||||
if (_playerProvider != null)
|
||||
{
|
||||
_playerProvider.OnRoomChanged -= OnRoomChanged;
|
||||
_playerProvider = null;
|
||||
}
|
||||
if (_mapSvc != null)
|
||||
{
|
||||
_mapSvc.OnDatabaseChanged -= OnDatabaseChanged;
|
||||
_mapSvc.OnExplorationChanged -= OnExplorationChanged;
|
||||
_mapSvc = null;
|
||||
}
|
||||
_pinService = null;
|
||||
}
|
||||
|
||||
private void ClearAllCells()
|
||||
{
|
||||
// R11-N5 禁用入池而非销毁
|
||||
foreach (var cell in _cells.Values)
|
||||
if (cell != null) Destroy(cell.gameObject);
|
||||
{
|
||||
if (cell == null) continue;
|
||||
cell.gameObject.SetActive(false);
|
||||
_cellPool.Push(cell);
|
||||
}
|
||||
_cells.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建格子坐标 → 房间 ID 的哈希映射。
|
||||
/// 将 RefreshView step② 从 O(allRooms) 全量遍历降至 O(viewRadius²) 范围格点查询。
|
||||
/// 数据库变更时(如热更)应再次调用。
|
||||
/// </summary>
|
||||
private void BuildSpatialIndex(MapDatabaseSO db)
|
||||
private void ClearPins()
|
||||
{
|
||||
_spatialIndex = new Dictionary<Vector2Int, string>();
|
||||
if (db?.AllRooms == null) return;
|
||||
foreach (var room in db.AllRooms)
|
||||
// R10-N3 禁用入池而非销毁
|
||||
foreach (var img in _pinImages)
|
||||
{
|
||||
if (room == null) continue;
|
||||
for (int x = 0; x < room.GridSize.x; x++)
|
||||
for (int y = 0; y < room.GridSize.y; y++)
|
||||
_spatialIndex[new Vector2Int(room.GridPosition.x + x, room.GridPosition.y + y)] = room.RoomId;
|
||||
if (img == null) continue;
|
||||
img.gameObject.SetActive(false);
|
||||
_pinPool.Push(img);
|
||||
}
|
||||
_pinImages.Clear();
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
// R21-N1 服务懒加载重试:_servicesReady 置 true 后短路,消除每帧 ServiceLocator 查询(对齐 MapPanel)
|
||||
if (!_servicesReady)
|
||||
SubscribeServices();
|
||||
UpdatePlayerDot();
|
||||
RenderPinsIfDirty();
|
||||
}
|
||||
|
||||
// ── 事件响应 ──────────────────────────────────────────────────────────
|
||||
|
||||
private void OnRoomChanged(string _) => RefreshView();
|
||||
|
||||
private void OnMapUpdated(string roomId)
|
||||
private void OnRoomChanged(string _)
|
||||
{
|
||||
if (_cells.TryGetValue(roomId, out var cell))
|
||||
cell.SetVisibility(_mapSvc.GetVisibility(roomId));
|
||||
// R10-N2 禁用时累积 dirty;OnEnable 后再 RefreshView
|
||||
if (!isActiveAndEnabled) { _viewDirty = true; return; }
|
||||
RefreshView();
|
||||
}
|
||||
|
||||
/// <summary>R10-N12 探索进度变化:仅刷新已实例化的格子可见性。</summary>
|
||||
private void OnExplorationChanged()
|
||||
{
|
||||
if (!isActiveAndEnabled) { _viewDirty = true; return; }
|
||||
if (_mapSvc == null) return;
|
||||
foreach (var (id, cell) in _cells)
|
||||
if (cell != null) cell.SetVisibility(_mapSvc.GetVisibility(id));
|
||||
}
|
||||
|
||||
/// <summary>数据库结构变更时完整重建:清空所有格子,下次 RefreshView 重新实例化。</summary>
|
||||
private void OnDatabaseChanged()
|
||||
{
|
||||
if (!isActiveAndEnabled) { _databaseDirty = true; return; }
|
||||
ClearAllCells();
|
||||
ClearPins();
|
||||
_lastPinVersion = -1;
|
||||
RefreshView();
|
||||
}
|
||||
|
||||
// ── Pin 渲染(可视范围内)─────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 仅渲染当前小地图视野内(已实例化格子的房间)的 Pin。
|
||||
/// 基于 PinsVersion + RefreshView 时机做脏检查:版本未变化且格子未变化时跳过。
|
||||
/// 单地图 N(视野)级别开销,远小于 MapPanel 的全图 Pin 渲染。
|
||||
/// </summary>
|
||||
private void RenderPinsIfDirty()
|
||||
{
|
||||
if (_pinService == null || _pinPrefab == null) return;
|
||||
if (_pinService.PinsVersion == _lastPinVersion) return;
|
||||
_lastPinVersion = _pinService.PinsVersion;
|
||||
RebuildPins();
|
||||
}
|
||||
|
||||
private void RebuildPins()
|
||||
{
|
||||
ClearPins();
|
||||
foreach (var pin in _pinService.Pins)
|
||||
{
|
||||
if (!_cells.TryGetValue(pin.RoomId, out var cell)) continue;
|
||||
// R10-N3 优先从对象池取
|
||||
Image img;
|
||||
if (_pinPool.Count > 0)
|
||||
{
|
||||
img = _pinPool.Pop();
|
||||
img.gameObject.SetActive(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
img = Instantiate(_pinPrefab, _cellContainer);
|
||||
}
|
||||
img.sprite = _pinConfig != null ? _pinConfig.GetSprite((PinType)pin.PinTypeInt) : null;
|
||||
img.rectTransform.anchoredPosition = cell.RT.anchoredPosition
|
||||
+ new Vector2(pin.NormalizedPosX * cell.RT.sizeDelta.x,
|
||||
pin.NormalizedPosY * cell.RT.sizeDelta.y);
|
||||
_pinImages.Add(img);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 视图重建 ──────────────────────────────────────────────────────────
|
||||
@@ -153,42 +300,56 @@ namespace BaseGames.World.Map
|
||||
var r = db.GetRoom(id);
|
||||
if (r == null || !RoomInView(r, minX, maxX, minY, maxY))
|
||||
{
|
||||
if (cell != null) Destroy(cell.gameObject);
|
||||
// R11-N5 禁用入池而非 Destroy,下次复用避免 GC 抖动
|
||||
if (cell != null)
|
||||
{
|
||||
cell.gameObject.SetActive(false);
|
||||
_cellPool.Push(cell);
|
||||
}
|
||||
_toRemove.Add(id);
|
||||
}
|
||||
}
|
||||
foreach (var id in _toRemove) _cells.Remove(id);
|
||||
|
||||
// ② 用空间索引替代 O(N) 全量遍历,在可视范围格点上查询所属房间
|
||||
// ② 通过 MapDatabaseSO 共享空间索引,在可视范围格点上查询所属房间
|
||||
// 复杂度:O(viewRadius²) 替代 O(allRooms),大地图下效果显著
|
||||
_roomsInViewBuffer.Clear();
|
||||
if (_spatialIndex != null)
|
||||
for (int x = minX; x <= maxX; x++)
|
||||
for (int y = minY; y <= maxY; y++)
|
||||
{
|
||||
for (int x = minX; x <= maxX; x++)
|
||||
for (int y = minY; y <= maxY; y++)
|
||||
{
|
||||
if (_spatialIndex.TryGetValue(new Vector2Int(x, y), out var rId))
|
||||
_roomsInViewBuffer.Add(rId);
|
||||
}
|
||||
var rId = db.GetRoomIdAtCell(new Vector2Int(x, y));
|
||||
if (!string.IsNullOrEmpty(rId)) _roomsInViewBuffer.Add(rId);
|
||||
}
|
||||
|
||||
_newlyAddedBuffer.Clear();
|
||||
foreach (var roomId in _roomsInViewBuffer)
|
||||
{
|
||||
if (_cells.ContainsKey(roomId)) continue;
|
||||
var room = db.GetRoom(roomId);
|
||||
if (room == null) continue;
|
||||
|
||||
var cell = Instantiate(_cellPrefab, _cellContainer);
|
||||
cell.Setup(room, _mapSvc.GetVisibility(room.RoomId), null);
|
||||
// R11-N5 优先从对象池取,避免高频 Instantiate
|
||||
MapRoomCellUI cell;
|
||||
if (_cellPool.Count > 0)
|
||||
{
|
||||
cell = _cellPool.Pop();
|
||||
cell.gameObject.SetActive(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
cell = Instantiate(_cellPrefab, _cellContainer);
|
||||
}
|
||||
cell.Setup(room, _mapSvc.GetVisibility(room.RoomId), ChooseIcon(room));
|
||||
cell.SetColors(_colorExplored, _colorMapped, _colorUnknown);
|
||||
PlaceCell(cell, room); // 立即设置正确的中心相对坐标,避免 Setup 默认偏移被 step③ 覆盖
|
||||
PlaceCell(cell, room); // 立即设置正确的中心相对坐标
|
||||
_cells[roomId] = cell;
|
||||
_newlyAddedBuffer.Add(roomId);
|
||||
}
|
||||
|
||||
// ③ 重定位所有格子(中心发生变化时)
|
||||
// ③ 重定位存量格子(新增格子在 step② 已 PlaceCell,跳过避免重复写入)
|
||||
foreach (var (id, cell) in _cells)
|
||||
{
|
||||
if (cell == null) continue;
|
||||
if (cell == null || _newlyAddedBuffer.Contains(id)) continue;
|
||||
var r = db.GetRoom(id);
|
||||
if (r != null) PlaceCell(cell, r);
|
||||
}
|
||||
@@ -206,6 +367,13 @@ namespace BaseGames.World.Map
|
||||
(room.GridPosition.y - _currentCenter.y) * _cellPixels);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R20-N2 委托到 MapRoomDataSO.ChooseDisplayIcon,消除与 MapPanel 的重复实现。
|
||||
/// 优先级:MapIconOverride > SavePoint > BossRoom > Shop > TeleportStation。
|
||||
/// </summary>
|
||||
private Sprite ChooseIcon(MapRoomDataSO room)
|
||||
=> room.ChooseDisplayIcon(_iconSavePoint, _iconBossRoom, _iconShop, _iconTeleport);
|
||||
|
||||
private static bool RoomInView(MapRoomDataSO room, int minX, int maxX, int minY, int maxY)
|
||||
=> room.GridPosition.x + room.GridSize.x > minX &&
|
||||
room.GridPosition.x < maxX &&
|
||||
@@ -237,5 +405,29 @@ namespace BaseGames.World.Map
|
||||
cell.RT.anchoredPosition
|
||||
+ Vector2.Scale(normPos, cell.RT.sizeDelta);
|
||||
}
|
||||
|
||||
// ── 缩放档位切换 ─────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// R12-FA 循环切换视野半径档位(可绑定到按键/按钮)。
|
||||
/// 档位在 Inspector 中通过 _zoomLevels 数组配置(默认:2/3/5 格)。
|
||||
/// 安全检查:数组为空时不切换,避免除零或越界。
|
||||
/// </summary>
|
||||
public void CycleZoom()
|
||||
{
|
||||
if (_zoomLevels == null || _zoomLevels.Length == 0) return;
|
||||
_zoomLevelIndex = (_zoomLevelIndex + 1) % _zoomLevels.Length;
|
||||
_viewRadiusCells = _zoomLevels[_zoomLevelIndex];
|
||||
if (isActiveAndEnabled)
|
||||
{
|
||||
// R29-N1 先清除标志再刷新,防止 HUD 关闭后重新打开触发冗余 RefreshView
|
||||
_viewDirty = false;
|
||||
RefreshView();
|
||||
}
|
||||
else
|
||||
{
|
||||
_viewDirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user