Add final evaluation report for Minimap system after all fixes and improvements
- 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
This commit is contained in:
@@ -2,7 +2,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityEngine.EventSystems;
|
||||
using TMPro;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
@@ -10,16 +9,17 @@ using BaseGames.Core.Save;
|
||||
|
||||
namespace BaseGames.World.Map
|
||||
{
|
||||
/// <summary>房间可见性三级状态:未知 / 已踏入 / 已标注(购买地图碎片)。</summary>
|
||||
public enum RoomVisibility { Unknown, Explored, Mapped }
|
||||
|
||||
/// <summary>
|
||||
/// 全屏地图 UI 面板(架构 15_MapShopModule §1.3)。
|
||||
/// 由 UIManager PanelStack 管理开关;OnEnable 时重建格子并订阅更新事件。
|
||||
/// <para>
|
||||
/// 依赖项均通过 <see cref="ServiceLocator"/> 获取(<see cref="IMapService"/>、
|
||||
/// <see cref="IPlayerPositionProvider"/>、<see cref="IPinService"/>),
|
||||
/// 不持有任何具体 MonoBehaviour 的 SerializeField 引用,实现架构解耦。
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public class MapPanel : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private MapDatabaseSO _database;
|
||||
[SerializeField] private RectTransform _roomContainer; // 格子图放置根节点
|
||||
[SerializeField] private MapRoomCellUI _cellPrefab; // 地图格子预制
|
||||
[SerializeField] private Image _exitConnectorPrefab; // 出口连接线预制(小矩形 Image)
|
||||
@@ -37,13 +37,11 @@ namespace BaseGames.World.Map
|
||||
[SerializeField] private Color _colorUnknown = Color.black;
|
||||
|
||||
[Header("玩家位置")]
|
||||
[SerializeField] private MapPlayerTracker _playerTracker; // 挂在 Player 上的追踪器
|
||||
[SerializeField] private Image _playerIconImg; // _roomContainer 内的玩家图标
|
||||
[SerializeField] private Image _playerIconImg; // _roomContainer 内的玩家图标
|
||||
|
||||
[Header("地图标记")]
|
||||
[SerializeField] private Image _pinPrefab;
|
||||
[SerializeField] private PinSpriteEntry[] _pinSprites; // PinType → 对应 Sprite
|
||||
[SerializeField] private MapPinManager _pinManager;
|
||||
[SerializeField] private PinSpriteEntry[] _pinSprites; // PinType → 对应 Sprite(在 Inspector 中配置)
|
||||
|
||||
[Header("Tooltip")]
|
||||
[SerializeField] private GameObject _tooltipPanel;
|
||||
@@ -52,16 +50,34 @@ namespace BaseGames.World.Map
|
||||
[Header("Event Channels")]
|
||||
[SerializeField] private StringEventChannelSO _onMapUpdated; // 房间发现/标注时刷新
|
||||
|
||||
private Dictionary<string, MapRoomCellUI> _cells = new();
|
||||
private List<Image> _pinImages = new();
|
||||
private List<Image> _exitImages = new();
|
||||
private Dictionary<string, MapRoomCellUI> _cells = new();
|
||||
private List<Image> _pinImages = new();
|
||||
private List<Image> _exitImages= new();
|
||||
private string _highlightedRoomId;
|
||||
private IMapService _mapSvc; // 面板活跃期间缓存,避免高频 ServiceLocator 查询
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
private string _lastIconRoomId; // LateUpdate 脏标记
|
||||
private Vector2 _lastIconNormPos; // LateUpdate 脏标记
|
||||
private int _lastPinVersion = -1;
|
||||
|
||||
private IMapService _mapSvc;
|
||||
private IPlayerPositionProvider _playerProvider;
|
||||
private IPinService _pinService;
|
||||
private Dictionary<PinType, Sprite> _pinSpriteDict;
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// 预构建 PinType → Sprite 字典,将 GetPinSprite 从 O(N) 降至 O(1)
|
||||
_pinSpriteDict = new Dictionary<PinType, Sprite>();
|
||||
if (_pinSprites != null)
|
||||
foreach (var e in _pinSprites)
|
||||
_pinSpriteDict[e.PinType] = e.Sprite;
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
_mapSvc = ServiceLocator.GetOrDefault<IMapService>();
|
||||
_mapSvc = ServiceLocator.GetOrDefault<IMapService>();
|
||||
_playerProvider = ServiceLocator.GetOrDefault<IPlayerPositionProvider>();
|
||||
_pinService = ServiceLocator.GetOrDefault<IPinService>();
|
||||
|
||||
// 首次打开时建立格子;后续打开只刷新探索状态,跳过重复 Instantiate
|
||||
if (_cells.Count == 0)
|
||||
@@ -78,7 +94,11 @@ namespace BaseGames.World.Map
|
||||
private void OnDisable()
|
||||
{
|
||||
_subs.Clear();
|
||||
_mapSvc = null;
|
||||
_mapSvc = null;
|
||||
_playerProvider = null;
|
||||
_pinService = null;
|
||||
_lastIconRoomId = null;
|
||||
_lastIconNormPos = Vector2.zero;
|
||||
HideTooltip();
|
||||
}
|
||||
|
||||
@@ -93,6 +113,12 @@ namespace BaseGames.World.Map
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (_playerProvider == null || _playerIconImg == null) return;
|
||||
// 脏标记:位置/房间未变化时跳过 RectTransform 读写,消除无效每帧开销
|
||||
if (_playerProvider.CurrentRoomId == _lastIconRoomId &&
|
||||
_playerProvider.NormalizedPositionInRoom == _lastIconNormPos) return;
|
||||
_lastIconRoomId = _playerProvider.CurrentRoomId;
|
||||
_lastIconNormPos = _playerProvider.NormalizedPositionInRoom;
|
||||
UpdatePlayerIcon();
|
||||
}
|
||||
|
||||
@@ -102,7 +128,7 @@ namespace BaseGames.World.Map
|
||||
foreach (var (roomId, cell) in _cells)
|
||||
{
|
||||
if (cell == null) continue;
|
||||
cell.SetVisibility(GetVisibility(_mapSvc, roomId));
|
||||
cell.SetVisibility(_mapSvc.GetVisibility(roomId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,13 +136,15 @@ namespace BaseGames.World.Map
|
||||
|
||||
private void BuildGrid()
|
||||
{
|
||||
if (_database?.AllRooms == null) return;
|
||||
foreach (var room in _database.AllRooms)
|
||||
var db = _mapSvc?.Database;
|
||||
if (db?.AllRooms == null) return;
|
||||
foreach (var room in db.AllRooms)
|
||||
{
|
||||
if (room == null) continue;
|
||||
var cell = Instantiate(_cellPrefab, _roomContainer);
|
||||
cell.Setup(room, GetVisibility(_mapSvc, room.RoomId), ChooseIcon(room),
|
||||
cell.Setup(room, _mapSvc.GetVisibility(room.RoomId), ChooseIcon(room),
|
||||
ShowTooltip, HideTooltip);
|
||||
cell.SetColors(_colorExplored, _colorMapped, _colorUnknown);
|
||||
_cells[room.RoomId] = cell;
|
||||
}
|
||||
DrawExits();
|
||||
@@ -125,16 +153,18 @@ namespace BaseGames.World.Map
|
||||
/// <summary>为每条出口在格子坐标处实例化一个小矩形连接线图像。</summary>
|
||||
private void DrawExits()
|
||||
{
|
||||
if (_exitConnectorPrefab == null || _database?.AllRooms == null) return;
|
||||
var db = _mapSvc?.Database;
|
||||
if (_exitConnectorPrefab == null || db?.AllRooms == null) return;
|
||||
ClearExits();
|
||||
const float px = 32f;
|
||||
foreach (var room in _database.AllRooms)
|
||||
foreach (var room in db.AllRooms)
|
||||
{
|
||||
if (room?.Exits == null) continue;
|
||||
foreach (var exit in room.Exits)
|
||||
{
|
||||
var conn = Instantiate(_exitConnectorPrefab, _roomContainer);
|
||||
conn.rectTransform.anchoredPosition = new Vector2(exit.ExitGridPos.x * px, exit.ExitGridPos.y * px);
|
||||
conn.rectTransform.anchoredPosition = new Vector2(
|
||||
exit.ExitGridPos.x * MapGridConstants.FullMapCellPixels,
|
||||
exit.ExitGridPos.y * MapGridConstants.FullMapCellPixels);
|
||||
bool vertical = exit.Direction == ExitDirection.Up || exit.Direction == ExitDirection.Down;
|
||||
conn.rectTransform.sizeDelta = vertical ? new Vector2(16f, 8f) : new Vector2(8f, 16f);
|
||||
_exitImages.Add(conn);
|
||||
@@ -152,30 +182,30 @@ namespace BaseGames.World.Map
|
||||
private void OnMapUpdated(string roomId)
|
||||
{
|
||||
if (_cells.TryGetValue(roomId, out var cell))
|
||||
cell.SetVisibility(GetVisibility(_mapSvc, roomId));
|
||||
cell.SetVisibility(_mapSvc.GetVisibility(roomId));
|
||||
}
|
||||
|
||||
// ── 玩家位置图标 ──────────────────────────────────────────────────────
|
||||
|
||||
private void UpdatePlayerIcon()
|
||||
{
|
||||
if (_playerIconImg == null || _playerTracker == null) return;
|
||||
var roomId = _playerTracker.CurrentRoomId;
|
||||
if (_playerIconImg == null || _playerProvider == null) return;
|
||||
var roomId = _playerProvider.CurrentRoomId;
|
||||
if (string.IsNullOrEmpty(roomId) || !_cells.TryGetValue(roomId, out var cell))
|
||||
{
|
||||
_playerIconImg.enabled = false;
|
||||
UpdateCellHighlight(null);
|
||||
return;
|
||||
}
|
||||
var cellRT = cell.GetComponent<RectTransform>();
|
||||
_playerIconImg.sprite = _iconPlayerPos;
|
||||
_playerIconImg.enabled = true;
|
||||
_playerIconImg.rectTransform.anchoredPosition =
|
||||
cellRT.anchoredPosition
|
||||
+ Vector2.Scale(_playerTracker.NormalizedPositionInRoom, cellRT.sizeDelta);
|
||||
cell.RT.anchoredPosition
|
||||
+ Vector2.Scale(_playerProvider.NormalizedPositionInRoom, cell.RT.sizeDelta);
|
||||
UpdateCellHighlight(roomId);
|
||||
}
|
||||
// ── 当前房间高亮 & ScrollRect 居中 ──────────────────────────────────────
|
||||
|
||||
// ── 当前房间高亮 & ScrollRect 居中 ─────────────────────────────────
|
||||
|
||||
/// <summary>切换高亮描边:取消旧房间高亮,激活新房间高亮。</summary>
|
||||
private void UpdateCellHighlight(string roomId)
|
||||
@@ -191,20 +221,20 @@ namespace BaseGames.World.Map
|
||||
/// <summary>面板打开时将 ScrollRect 视口居中到玩家当前所在房间。</summary>
|
||||
private void CenterOnCurrentRoom()
|
||||
{
|
||||
if (_scrollRect == null || _playerTracker == null) return;
|
||||
var roomId = _playerTracker.CurrentRoomId;
|
||||
if (_scrollRect == null || _playerProvider == null) return;
|
||||
var roomId = _playerProvider.CurrentRoomId;
|
||||
if (string.IsNullOrEmpty(roomId) || !_cells.TryGetValue(roomId, out var cell)) return;
|
||||
|
||||
Canvas.ForceUpdateCanvases();
|
||||
// 仅重建 ScrollRect.content 的布局,避免全 Canvas 树强制刷新
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(_scrollRect.content);
|
||||
|
||||
var content = _scrollRect.content;
|
||||
var viewport = _scrollRect.viewport != null
|
||||
? _scrollRect.viewport
|
||||
: (RectTransform)_scrollRect.transform;
|
||||
var cellRT = cell.GetComponent<RectTransform>();
|
||||
|
||||
// 将 cell 中心转换到 content 本地坐标系
|
||||
Vector2 cellWorldCenter = cellRT.TransformPoint(cellRT.rect.center);
|
||||
Vector2 cellWorldCenter = cell.RT.TransformPoint(cell.RT.rect.center);
|
||||
Vector2 cellLocal = content.InverseTransformPoint(cellWorldCenter);
|
||||
|
||||
// 距 content 左下角的距离(pivot 无关)
|
||||
@@ -221,22 +251,28 @@ namespace BaseGames.World.Map
|
||||
|
||||
_scrollRect.normalizedPosition = new Vector2(normX, normY);
|
||||
}
|
||||
|
||||
// ── 地图标记渲染 ──────────────────────────────────────────────────────
|
||||
|
||||
private void RenderPins()
|
||||
{
|
||||
if (_pinService == null) return;
|
||||
|
||||
// 版本号脏检查:Pin 集合未变化时跳过重绘,避免无效 Instantiate
|
||||
if (_pinService.PinsVersion == _lastPinVersion && _pinImages.Count > 0) return;
|
||||
_lastPinVersion = _pinService.PinsVersion;
|
||||
|
||||
ClearPins();
|
||||
if (_pinPrefab == null || _pinManager == null) return;
|
||||
foreach (var pin in _pinManager.Pins)
|
||||
if (_pinPrefab == null) return;
|
||||
foreach (var pin in _pinService.Pins)
|
||||
{
|
||||
if (!_cells.TryGetValue(pin.RoomId, out var cell)) continue;
|
||||
var img = Instantiate(_pinPrefab, _roomContainer);
|
||||
img.sprite = GetPinSprite((PinType)pin.PinTypeInt);
|
||||
var cellRT = cell.GetComponent<RectTransform>();
|
||||
img.rectTransform.anchoredPosition =
|
||||
cellRT.anchoredPosition + new Vector2(
|
||||
pin.NormalizedPosX * cellRT.sizeDelta.x,
|
||||
pin.NormalizedPosY * cellRT.sizeDelta.y);
|
||||
cell.RT.anchoredPosition + new Vector2(
|
||||
pin.NormalizedPosX * cell.RT.sizeDelta.x,
|
||||
pin.NormalizedPosY * cell.RT.sizeDelta.y);
|
||||
_pinImages.Add(img);
|
||||
}
|
||||
}
|
||||
@@ -261,15 +297,6 @@ namespace BaseGames.World.Map
|
||||
|
||||
// ── 辅助方法 ──────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>按优先级推导可见性:Explored > Mapped > Unknown。</summary>
|
||||
private static RoomVisibility GetVisibility(IMapService svc, string roomId)
|
||||
{
|
||||
if (svc == null) return RoomVisibility.Unknown;
|
||||
if (svc.IsExplored(roomId)) return RoomVisibility.Explored;
|
||||
if (svc.IsMapped(roomId)) return RoomVisibility.Mapped;
|
||||
return RoomVisibility.Unknown;
|
||||
}
|
||||
|
||||
private Sprite ChooseIcon(MapRoomDataSO room)
|
||||
{
|
||||
if (room.MapIconOverride != null) return room.MapIconOverride;
|
||||
@@ -280,95 +307,6 @@ namespace BaseGames.World.Map
|
||||
}
|
||||
|
||||
private Sprite GetPinSprite(PinType type)
|
||||
{
|
||||
if (_pinSprites != null)
|
||||
foreach (var e in _pinSprites)
|
||||
if (e.PinType == type) return e.Sprite;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public struct PinSpriteEntry
|
||||
{
|
||||
public PinType PinType;
|
||||
public Sprite Sprite;
|
||||
}
|
||||
|
||||
// ─── 单个地图格子 UI ─────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>地图面板中每个房间对应的格子 UI 组件。</summary>
|
||||
public class MapRoomCellUI : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler
|
||||
{
|
||||
[SerializeField] private Image _bg;
|
||||
[SerializeField] private Image _icon;
|
||||
[SerializeField] private RawImage _outlineImage; // 可选:房间非矩形轮廓纹理
|
||||
[SerializeField] private Image _highlight; // 可选:当前房间高亮描边(玩家所在时激活)
|
||||
|
||||
private static readonly Color ColExplored = Color.white;
|
||||
private static readonly Color ColMapped = new Color(0.45f, 0.45f, 0.45f, 1f);
|
||||
private static readonly Color ColUnknown = Color.black;
|
||||
|
||||
private string _displayName;
|
||||
private Action<string> _onHover;
|
||||
private Action _onHoverExit;
|
||||
|
||||
/// <summary>初始化格子(位置、可见性、图标、Tooltip 回调)。</summary>
|
||||
public void Setup(MapRoomDataSO room, RoomVisibility visibility, Sprite icon,
|
||||
Action<string> onHover = null, Action onHoverExit = null)
|
||||
{
|
||||
_displayName = room.DisplayName;
|
||||
_onHover = onHover;
|
||||
_onHoverExit = onHoverExit;
|
||||
|
||||
if (TryGetComponent<RectTransform>(out var rt))
|
||||
{
|
||||
rt.anchoredPosition = new Vector2(room.GridPosition.x * 32f, room.GridPosition.y * 32f);
|
||||
rt.sizeDelta = new Vector2(room.GridSize.x * 32f, room.GridSize.y * 32f);
|
||||
}
|
||||
|
||||
// 房间轮廓纹理(非矩形形状,覆盖在矩形背景上方)
|
||||
if (_outlineImage != null)
|
||||
{
|
||||
_outlineImage.texture = room.RoomOutlineTex;
|
||||
_outlineImage.enabled = room.RoomOutlineTex != null;
|
||||
}
|
||||
|
||||
SetVisibility(visibility);
|
||||
|
||||
if (_icon != null)
|
||||
{
|
||||
_icon.sprite = icon;
|
||||
_icon.enabled = icon != null;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetVisibility(RoomVisibility v)
|
||||
{
|
||||
if (_bg == null) return;
|
||||
_bg.color = v switch
|
||||
{
|
||||
RoomVisibility.Explored => ColExplored,
|
||||
RoomVisibility.Mapped => ColMapped,
|
||||
_ => ColUnknown,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>向后兼容:直接传 bool 时等同于 Explored / Unknown。</summary>
|
||||
public void SetDiscovered(bool v)
|
||||
=> SetVisibility(v ? RoomVisibility.Explored : RoomVisibility.Unknown);
|
||||
|
||||
/// <summary>激活/取消当前房间高亮描边。</summary>
|
||||
public void SetHighlight(bool v)
|
||||
{
|
||||
if (_highlight != null) _highlight.enabled = v;
|
||||
}
|
||||
|
||||
public void OnPointerEnter(PointerEventData _)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_displayName)) _onHover?.Invoke(_displayName);
|
||||
}
|
||||
|
||||
public void OnPointerExit(PointerEventData _) => _onHoverExit?.Invoke();
|
||||
=> _pinSpriteDict.TryGetValue(type, out var s) ? s : null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user