using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using BaseGames.Core; using BaseGames.Core.Events; namespace BaseGames.World.Map { /// /// 角落小地图 HUD(架构 15_MapShopModule §1.6)。 /// 以玩家当前房间为中心,仅渲染 ±ViewRadiusCells 格范围内的房间。 /// 玩家跨格进入新房间时(OnRoomChanged 事件)触发增量重建,无需每帧扫描全局。 /// /// 挂载位置:HUD Canvas 下 Minimap 根节点(需配有 RectMask2D 用于裁剪)。 /// /// 依赖 , /// 均通过 获取,不持有具体类的 SerializeField 引用。 /// public class MinimapHUD : MonoBehaviour { [SerializeField] private MapRoomCellUI _cellPrefab; [SerializeField] private RectTransform _cellContainer; // 带 RectMask2D 的容器,内容在此节点内平移 [SerializeField] private Image _playerDot; // 玩家位置圆点(在 _cellContainer 内) [Header("显示范围")] [SerializeField, Min(1)] private int _viewRadiusCells = 3; // 以玩家房间中心为圆心的可视半径(格) [SerializeField] private float _cellPixels = 16f; // 每格显示像素数 [Header("颜色(Inspector 覆盖)")] [SerializeField] private Color _colorExplored = Color.white; [SerializeField] private Color _colorMapped = new Color(0.45f, 0.45f, 0.45f, 1f); [SerializeField] private Color _colorUnknown = Color.black; [Header("Event Channels")] [SerializeField] private StringEventChannelSO _onMapUpdated; // 房间发现/标注时局部刷新 private IMapService _mapSvc; private IPlayerPositionProvider _playerProvider; private readonly Dictionary _cells = new(); private readonly CompositeDisposable _subs = new(); // 复用 List 避免 RefreshView 每次分配临时 List(GC 友好) private readonly List _toRemove = new List(8); private Vector2Int _currentCenter; private string _lastDotRoomId; private Vector2 _lastDotNormPos; // ── 生命周期 ────────────────────────────────────────────────────────── private void OnEnable() { _mapSvc = ServiceLocator.GetOrDefault(); _playerProvider = ServiceLocator.GetOrDefault(); if (_playerProvider != null) _playerProvider.OnRoomChanged += OnRoomChanged; _onMapUpdated?.Subscribe(OnMapUpdated).AddTo(_subs); // 首次显示时立即刷新 RefreshView(); } private void OnDisable() { if (_playerProvider != null) _playerProvider.OnRoomChanged -= OnRoomChanged; _subs.Clear(); ClearAllCells(); _lastDotRoomId = null; _mapSvc = null; _playerProvider = null; } private void ClearAllCells() { foreach (var cell in _cells.Values) if (cell != null) Destroy(cell.gameObject); _cells.Clear(); } private void LateUpdate() { UpdatePlayerDot(); } // ── 事件响应 ────────────────────────────────────────────────────────── private void OnRoomChanged(string _) => RefreshView(); private void OnMapUpdated(string roomId) { if (_cells.TryGetValue(roomId, out var cell)) cell.SetVisibility(_mapSvc.GetVisibility(roomId)); } // ── 视图重建 ────────────────────────────────────────────────────────── /// /// 以玩家当前房间中心为基准,增量更新可视格内的格子: /// 回收超出范围的旧格子,实例化刚进入范围的新格子,重定位全部格子到新中心。 /// private void RefreshView() { var db = _mapSvc?.Database; if (db?.AllRooms == null) return; var currentRoomId = _playerProvider?.CurrentRoomId; if (string.IsNullOrEmpty(currentRoomId)) return; var currentRoom = db.GetRoom(currentRoomId); if (currentRoom == null) return; _currentCenter = currentRoom.GridPosition + currentRoom.GridSize / 2; int minX = _currentCenter.x - _viewRadiusCells; int maxX = _currentCenter.x + _viewRadiusCells; int minY = _currentCenter.y - _viewRadiusCells; int maxY = _currentCenter.y + _viewRadiusCells; // ① 回收不在可视范围内的格子(复用 _toRemove 避免每帧 new) _toRemove.Clear(); foreach (var (id, cell) in _cells) { var r = db.GetRoom(id); if (r == null || !RoomInView(r, minX, maxX, minY, maxY)) { if (cell != null) Destroy(cell.gameObject); _toRemove.Add(id); } } foreach (var id in _toRemove) _cells.Remove(id); // ② 实例化新进入范围的格子 foreach (var room in db.AllRooms) { if (room == null || _cells.ContainsKey(room.RoomId)) continue; if (!RoomInView(room, minX, maxX, minY, maxY)) continue; var cell = Instantiate(_cellPrefab, _cellContainer); cell.Setup(room, _mapSvc.GetVisibility(room.RoomId), null); cell.SetColors(_colorExplored, _colorMapped, _colorUnknown); _cells[room.RoomId] = cell; } // ③ 重定位所有格子(中心发生变化时) foreach (var (id, cell) in _cells) { if (cell == null) continue; var r = db.GetRoom(id); if (r != null) PlaceCell(cell, r); } UpdatePlayerDot(); } // ── 格子位置 ────────────────────────────────────────────────────────── private void PlaceCell(MapRoomCellUI cell, MapRoomDataSO room) { cell.RT.sizeDelta = new Vector2(room.GridSize.x * _cellPixels, room.GridSize.y * _cellPixels); cell.RT.anchoredPosition = new Vector2( (room.GridPosition.x - _currentCenter.x) * _cellPixels, (room.GridPosition.y - _currentCenter.y) * _cellPixels); } 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 && room.GridPosition.y + room.GridSize.y > minY && room.GridPosition.y < maxY; // ── 玩家圆点 ────────────────────────────────────────────────────────── private void UpdatePlayerDot() { if (_playerDot == null || _playerProvider == null) return; var roomId = _playerProvider.CurrentRoomId; var normPos = _playerProvider.NormalizedPositionInRoom; // Dirty check: 避免房间和位置均未变化时写 RectTransform if (roomId == _lastDotRoomId && normPos == _lastDotNormPos) return; _lastDotRoomId = roomId; _lastDotNormPos = normPos; if (string.IsNullOrEmpty(roomId) || !_cells.TryGetValue(roomId, out var cell)) { _playerDot.enabled = false; return; } _playerDot.enabled = true; _playerDot.rectTransform.anchoredPosition = cell.RT.anchoredPosition + Vector2.Scale(normPos, cell.RT.sizeDelta); } } }