using System;
using UnityEngine;
using BaseGames.Core;
namespace BaseGames.World.Map
{
///
/// 将玩家世界坐标转换为地图格子坐标,供 MapPanel / MinimapHUD 显示玩家位置图标。
/// 挂在 Player GameObject 上(LateUpdate 每帧计算)。
///
/// 性能:通过 共享空间索引,
/// LateUpdate 房间判定为 O(1) 哈希查找。归一化位置每帧从世界坐标精确插值,
/// 确保图标跟随玩家平滑移动,而非以格子步长离散跳动。
///
/// 通过 注册为 。
///
[DefaultExecutionOrder(-600)] // 晚于 MapManager(-700),早于默认 0,确保 IPlayerPositionProvider 在 MinimapHUD.Awake 前已注册
public class MapPlayerTracker : MonoBehaviour, IPlayerPositionProvider
{
[SerializeField] private Transform _playerTransform;
[SerializeField] private MapDatabaseSO _databaseOverride; // 可选:直接指定;留空则从 IMapService 获取
[Header("世界坐标 → 格子坐标换算参数")]
[Tooltip("1 格对应的世界单位数。请在关卡编辑器中测量房间实际尺寸后填入,确保与关卡设计对齐。")]
[SerializeField, Min(0.01f)] private float _worldUnitsPerCell = 18f;
[Tooltip("R11-N9 世界原点偏移量(世界单位)。\n" +
"当关卡世界坐标原点不在格子地图 (0,0) 处时,填入偏移量以对齐格子坐标系。\n" +
"例如:关卡第一个房间的世界左下角在 (-36, -18) 时,此处填 (-36, -18)。")]
[SerializeField] private Vector2 _worldOriginOffset = Vector2.zero;
/// 玩家当前所在房间 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 MapRoomDataSO _currentRoom; // 当前房间数据缓存,避免 LateUpdate 每帧 GetRoom 查找
private Vector2Int _lastCellPos = new Vector2Int(int.MinValue, int.MinValue);
private bool _isDuplicate; // Awake 检测到重复实例时置位,Start/LateUpdate/OnDestroy 提前 return
private void Awake()
{
// 单例保护:同一时刻只允许一个 IPlayerPositionProvider 存在
if (ServiceLocator.GetOrDefault() != null)
{
_isDuplicate = true;
Destroy(gameObject);
return;
}
ServiceLocator.Register(this);
}
private void Start()
{
if (_isDuplicate) return;
_database = _databaseOverride
?? ServiceLocator.GetOrDefault()?.Database;
}
private void OnDestroy()
{
if (_isDuplicate) return;
// ServiceLocator.Unregister(impl) 内部以 ReferenceEquals 守卫,
// 仅当当前注册实例即本实例时才移除,避免多个 Tracker 并存时误清。
ServiceLocator.Unregister(this);
}
private void LateUpdate()
{
if (_isDuplicate) return;
if (_playerTransform == null || _database == null) return;
Vector2Int cellPos = WorldToCell(_playerTransform.position);
bool cellChanged = cellPos != _lastCellPos;
if (cellChanged)
{
_lastCellPos = cellPos;
// 通过 MapDatabaseSO 共享的空间索引查询(O(1)),避免本组件重复构建索引
var newRoomId = _database.GetRoomIdAtCell(cellPos);
if (string.IsNullOrEmpty(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)
{
// R11-N9 减去 _worldOriginOffset 将世界坐标对齐格子坐标系原点
var worldMin = new Vector2(
_currentRoom.GridPosition.x * _worldUnitsPerCell + _worldOriginOffset.x,
_currentRoom.GridPosition.y * _worldUnitsPerCell + _worldOriginOffset.y);
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)
{
// R11-N9 减去 _worldOriginOffset 对齐格子坐标系,再除以单格世界尺寸
var adjusted = worldPos - _worldOriginOffset;
return new(
Mathf.FloorToInt(adjusted.x / _worldUnitsPerCell),
Mathf.FloorToInt(adjusted.y / _worldUnitsPerCell));
}
///
/// 将世界坐标转换为所属房间 ID 和房间内归一化位置(0~1)。
/// 实现 。
///
public bool TryGetRoomAtWorldPos(Vector3 worldPos, out string roomId, out Vector2 normalizedPos)
{
roomId = null;
normalizedPos = Vector2.zero;
if (_database == null) return false;
var cellPos = WorldToCell(worldPos);
roomId = _database.GetRoomIdAtCell(cellPos);
if (string.IsNullOrEmpty(roomId)) return false;
var room = _database.GetRoom(roomId);
if (room == null) return false;
var worldMin = new Vector2(
room.GridPosition.x * _worldUnitsPerCell + _worldOriginOffset.x,
room.GridPosition.y * _worldUnitsPerCell + _worldOriginOffset.y);
var worldSize = new Vector2(
room.GridSize.x * _worldUnitsPerCell,
room.GridSize.y * _worldUnitsPerCell);
var localPos = (Vector2)worldPos - worldMin;
normalizedPos = new Vector2(
Mathf.Clamp01(localPos.x / Mathf.Max(1f, worldSize.x)),
Mathf.Clamp01(localPos.y / Mathf.Max(1f, worldSize.y)));
return true;
}
}
}