Files
zeling_v2/Assets/_Game/Scripts/World/Map/MapPlayerTracker.cs
Joywayer 5cb6c2a19d 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
2026-05-25 14:25:19 +08:00

134 lines
5.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using UnityEngine;
using BaseGames.Core;
namespace BaseGames.World.Map
{
/// <summary>
/// 将玩家世界坐标转换为地图格子坐标,供 MapPanel / MinimapHUD 显示玩家位置图标。
/// 挂在 Player GameObject 上LateUpdate 每帧计算)。
/// <para>
/// 性能:首次启动时建立 Dictionary&lt;Vector2Int, string&gt; 空间索引,
/// LateUpdate 房间判定为 O(1) 哈希查找。归一化位置每帧从世界坐标精确插值,
/// 确保图标跟随玩家平滑移动,而非以格子步长离散跳动。
/// </para>
/// 通过 <see cref="ServiceLocator"/> 注册为 <see cref="IPlayerPositionProvider"/>。
/// </summary>
public class MapPlayerTracker : MonoBehaviour, IPlayerPositionProvider
{
[SerializeField] private Transform _playerTransform;
[SerializeField] private MapDatabaseSO _databaseOverride; // 可选:直接指定;留空则从 IMapService 获取
[Header("世界坐标 → 格子坐标换算参数")]
[Tooltip("1 格对应的世界单位数。请在关卡编辑器中测量房间实际尺寸后填入,确保与关卡设计对齐。")]
[SerializeField] private float _worldUnitsPerCell = 18f;
/// <summary>玩家当前所在房间 ID未在任何已知房间内时为 null。</summary>
public string CurrentRoomId { get; private set; }
/// <summary>
/// 玩家在当前房间内的归一化坐标0~1
/// 基于世界坐标精确插值,每帧更新,可用于平滑移动图标。
/// </summary>
public Vector2 NormalizedPositionInRoom { get; private set; }
/// <summary>玩家进入新房间时触发(参数为新房间 ID。</summary>
public event Action<string> OnRoomChanged;
private MapDatabaseSO _database;
private Dictionary<Vector2Int, string> _cellToRoomId;
private MapRoomDataSO _currentRoom; // 当前房间数据缓存,避免 LateUpdate 每帧 GetRoom 查找
private Vector2Int _lastCellPos = new Vector2Int(int.MinValue, int.MinValue);
private void Awake()
{
// 单例保护:同一时刻只允许一个 IPlayerPositionProvider 存在
if (ServiceLocator.GetOrDefault<IPlayerPositionProvider>() != null) return;
ServiceLocator.Register<IPlayerPositionProvider>(this);
}
private void Start()
{
_database = _databaseOverride
?? ServiceLocator.GetOrDefault<IMapService>()?.Database;
BuildSpatialIndex();
}
private void OnDestroy()
{
ServiceLocator.Unregister<IPlayerPositionProvider>(this);
}
/// <summary>
/// 构建格子坐标 → 房间 ID 的哈希映射,将 LateUpdate 的查询从 O(N) 降至 O(1)。
/// 房间数据变化时(运行时热更)可再次调用重建索引。
/// </summary>
public void BuildSpatialIndex()
{
_cellToRoomId = new Dictionary<Vector2Int, string>();
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));
}
}