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); // 空间索引:格子坐标 → 房间 ID,将 RefreshView step② 的 O(N) 遍历降至 O(viewRadius²) private Dictionary _spatialIndex; // 复用 HashSet 避免 RefreshView 每次分配(GC 友好) private readonly HashSet _roomsInViewBuffer = new HashSet(32); private Vector2Int _currentCenter; private string _lastDotRoomId; private Vector2 _lastDotNormPos; // ── 生命周期 ────────────────────────────────────────────────────────── private void OnEnable() { _mapSvc = ServiceLocator.GetOrDefault(); _playerProvider = ServiceLocator.GetOrDefault(); BuildSpatialIndex(_mapSvc?.Database); 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; _spatialIndex = null; } private void ClearAllCells() { foreach (var cell in _cells.Values) if (cell != null) Destroy(cell.gameObject); _cells.Clear(); } /// /// 构建格子坐标 → 房间 ID 的哈希映射。 /// 将 RefreshView step② 从 O(allRooms) 全量遍历降至 O(viewRadius²) 范围格点查询。 /// 数据库变更时(如热更)应再次调用。 /// private void BuildSpatialIndex(MapDatabaseSO db) { _spatialIndex = new Dictionary(); if (db?.AllRooms == null) return; foreach (var room in db.AllRooms) { 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; } } 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); // ② 用空间索引替代 O(N) 全量遍历,在可视范围格点上查询所属房间 // 复杂度:O(viewRadius²) 替代 O(allRooms),大地图下效果显著 _roomsInViewBuffer.Clear(); if (_spatialIndex != null) { 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); } } 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); cell.SetColors(_colorExplored, _colorMapped, _colorUnknown); PlaceCell(cell, room); // 立即设置正确的中心相对坐标,避免 Setup 默认偏移被 step③ 覆盖 _cells[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); } } }