using System; using System.Collections.Generic; using UnityEngine; using BaseGames.Core; namespace BaseGames.World.Map { /// /// 将玩家世界坐标转换为地图格子坐标,供 MapPanel / MinimapHUD 显示玩家位置图标。 /// 挂在 Player GameObject 上(LateUpdate 每帧计算)。 /// /// 性能:首次启动时建立 Dictionary<Vector2Int, string> 空间索引, /// LateUpdate 房间判定为 O(1) 哈希查找。归一化位置每帧从世界坐标精确插值, /// 确保图标跟随玩家平滑移动,而非以格子步长离散跳动。 /// /// 通过 注册为 。 /// public class MapPlayerTracker : MonoBehaviour, IPlayerPositionProvider { [SerializeField] private Transform _playerTransform; [SerializeField] private MapDatabaseSO _databaseOverride; // 可选:直接指定;留空则从 IMapService 获取 [Header("世界坐标 → 格子坐标换算参数")] [Tooltip("1 格对应的世界单位数。请在关卡编辑器中测量房间实际尺寸后填入,确保与关卡设计对齐。")] [SerializeField] private float _worldUnitsPerCell = 18f; /// 玩家当前所在房间 ID;未在任何已知房间内时为 null。 public string CurrentRoomId { get; private set; } /// /// 玩家在当前房间内的归一化坐标(0~1)。 /// 基于世界坐标精确插值,每帧更新,可用于平滑移动图标。 /// public Vector2 NormalizedPositionInRoom { get; private set; } /// 玩家进入新房间时触发(参数为新房间 ID)。 public event Action OnRoomChanged; private MapDatabaseSO _database; private Dictionary _cellToRoomId; private MapRoomDataSO _currentRoom; // 当前房间数据缓存,避免 LateUpdate 每帧 GetRoom 查找 private Vector2Int _lastCellPos = new Vector2Int(int.MinValue, int.MinValue); private void Awake() { // 单例保护:同一时刻只允许一个 IPlayerPositionProvider 存在 if (ServiceLocator.GetOrDefault() != null) return; ServiceLocator.Register(this); } private void Start() { _database = _databaseOverride ?? ServiceLocator.GetOrDefault()?.Database; BuildSpatialIndex(); } private void OnDestroy() { ServiceLocator.Unregister(this); } /// /// 构建格子坐标 → 房间 ID 的哈希映射,将 LateUpdate 的查询从 O(N) 降至 O(1)。 /// 房间数据变化时(运行时热更)可再次调用重建索引。 /// public void BuildSpatialIndex() { _cellToRoomId = new Dictionary(); if (_database?.AllRooms == null) return; foreach (var room in _database.AllRooms) { if (room == null) continue; for (int x = 0; x < room.GridSize.x; x++) for (int y = 0; y < room.GridSize.y; y++) { var cell = new Vector2Int(room.GridPosition.x + x, room.GridPosition.y + y); _cellToRoomId[cell] = room.RoomId; } } } private void LateUpdate() { if (_playerTransform == null || _cellToRoomId == null) return; Vector2Int cellPos = WorldToCell(_playerTransform.position); bool cellChanged = cellPos != _lastCellPos; if (cellChanged) { _lastCellPos = cellPos; if (!_cellToRoomId.TryGetValue(cellPos, out var newRoomId)) { // 玩家离开所有已知房间 CurrentRoomId = null; _currentRoom = null; NormalizedPositionInRoom = Vector2.zero; return; } var prevRoomId = CurrentRoomId; CurrentRoomId = newRoomId; _currentRoom = _database.GetRoom(newRoomId); if (newRoomId != prevRoomId) OnRoomChanged?.Invoke(newRoomId); } // 每帧从世界坐标精确计算归一化位置,实现平滑图标跟随 if (_currentRoom != null) { var worldMin = new Vector2( _currentRoom.GridPosition.x * _worldUnitsPerCell, _currentRoom.GridPosition.y * _worldUnitsPerCell); var worldSize = new Vector2( _currentRoom.GridSize.x * _worldUnitsPerCell, _currentRoom.GridSize.y * _worldUnitsPerCell); var localPos = (Vector2)_playerTransform.position - worldMin; NormalizedPositionInRoom = new Vector2( Mathf.Clamp01(localPos.x / Mathf.Max(1f, worldSize.x)), Mathf.Clamp01(localPos.y / Mathf.Max(1f, worldSize.y))); } } private Vector2Int WorldToCell(Vector2 worldPos) => new( Mathf.FloorToInt(worldPos.x / _worldUnitsPerCell), Mathf.FloorToInt(worldPos.y / _worldUnitsPerCell)); } }