Add independent review reports for Minimap system (Rounds 8, 9, and 26)
- Round 8 report highlights improvements in architecture, editor usability, and data robustness, with a total score of 80/100. - Round 9 report focuses on editor extension capabilities, identifying issues with room data indexing and layout editing, resulting in a score of 76/100. - Round 26 report evaluates the system against commercial standards, noting new issues and confirming previous fixes, with a score of 95.8/100.
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using BaseGames.Core;
|
||||
|
||||
@@ -9,12 +8,13 @@ namespace BaseGames.World.Map
|
||||
/// 将玩家世界坐标转换为地图格子坐标,供 MapPanel / MinimapHUD 显示玩家位置图标。
|
||||
/// 挂在 Player GameObject 上(LateUpdate 每帧计算)。
|
||||
/// <para>
|
||||
/// 性能:首次启动时建立 Dictionary<Vector2Int, string> 空间索引,
|
||||
/// 性能:通过 <see cref="MapDatabaseSO.GetRoomIdAtCell"/> 共享空间索引,
|
||||
/// LateUpdate 房间判定为 O(1) 哈希查找。归一化位置每帧从世界坐标精确插值,
|
||||
/// 确保图标跟随玩家平滑移动,而非以格子步长离散跳动。
|
||||
/// </para>
|
||||
/// 通过 <see cref="ServiceLocator"/> 注册为 <see cref="IPlayerPositionProvider"/>。
|
||||
/// </summary>
|
||||
[DefaultExecutionOrder(-600)] // 晚于 MapManager(-700),早于默认 0,确保 IPlayerPositionProvider 在 MinimapHUD.Awake 前已注册
|
||||
public class MapPlayerTracker : MonoBehaviour, IPlayerPositionProvider
|
||||
{
|
||||
[SerializeField] private Transform _playerTransform;
|
||||
@@ -22,7 +22,12 @@ namespace BaseGames.World.Map
|
||||
|
||||
[Header("世界坐标 → 格子坐标换算参数")]
|
||||
[Tooltip("1 格对应的世界单位数。请在关卡编辑器中测量房间实际尺寸后填入,确保与关卡设计对齐。")]
|
||||
[SerializeField] private float _worldUnitsPerCell = 18f;
|
||||
[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;
|
||||
|
||||
/// <summary>玩家当前所在房间 ID;未在任何已知房间内时为 null。</summary>
|
||||
public string CurrentRoomId { get; private set; }
|
||||
@@ -37,52 +42,41 @@ namespace BaseGames.World.Map
|
||||
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 bool _isDuplicate; // Awake 检测到重复实例时置位,Start/LateUpdate/OnDestroy 提前 return
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// 单例保护:同一时刻只允许一个 IPlayerPositionProvider 存在
|
||||
if (ServiceLocator.GetOrDefault<IPlayerPositionProvider>() != null) return;
|
||||
if (ServiceLocator.GetOrDefault<IPlayerPositionProvider>() != null)
|
||||
{
|
||||
_isDuplicate = true;
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
ServiceLocator.Register<IPlayerPositionProvider>(this);
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (_isDuplicate) return;
|
||||
_database = _databaseOverride
|
||||
?? ServiceLocator.GetOrDefault<IMapService>()?.Database;
|
||||
BuildSpatialIndex();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_isDuplicate) return;
|
||||
// ServiceLocator.Unregister<T>(impl) 内部以 ReferenceEquals 守卫,
|
||||
// 仅当当前注册实例即本实例时才移除,避免多个 Tracker 并存时误清。
|
||||
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;
|
||||
if (_isDuplicate) return;
|
||||
if (_playerTransform == null || _database == null) return;
|
||||
|
||||
Vector2Int cellPos = WorldToCell(_playerTransform.position);
|
||||
bool cellChanged = cellPos != _lastCellPos;
|
||||
@@ -91,7 +85,9 @@ namespace BaseGames.World.Map
|
||||
{
|
||||
_lastCellPos = cellPos;
|
||||
|
||||
if (!_cellToRoomId.TryGetValue(cellPos, out var newRoomId))
|
||||
// 通过 MapDatabaseSO 共享的空间索引查询(O(1)),避免本组件重复构建索引
|
||||
var newRoomId = _database.GetRoomIdAtCell(cellPos);
|
||||
if (string.IsNullOrEmpty(newRoomId))
|
||||
{
|
||||
// 玩家离开所有已知房间
|
||||
CurrentRoomId = null;
|
||||
@@ -111,9 +107,10 @@ namespace BaseGames.World.Map
|
||||
// 每帧从世界坐标精确计算归一化位置,实现平滑图标跟随
|
||||
if (_currentRoom != null)
|
||||
{
|
||||
// R11-N9 减去 _worldOriginOffset 将世界坐标对齐格子坐标系原点
|
||||
var worldMin = new Vector2(
|
||||
_currentRoom.GridPosition.x * _worldUnitsPerCell,
|
||||
_currentRoom.GridPosition.y * _worldUnitsPerCell);
|
||||
_currentRoom.GridPosition.x * _worldUnitsPerCell + _worldOriginOffset.x,
|
||||
_currentRoom.GridPosition.y * _worldUnitsPerCell + _worldOriginOffset.y);
|
||||
var worldSize = new Vector2(
|
||||
_currentRoom.GridSize.x * _worldUnitsPerCell,
|
||||
_currentRoom.GridSize.y * _worldUnitsPerCell);
|
||||
@@ -125,9 +122,43 @@ namespace BaseGames.World.Map
|
||||
}
|
||||
|
||||
private Vector2Int WorldToCell(Vector2 worldPos)
|
||||
=> new(
|
||||
Mathf.FloorToInt(worldPos.x / _worldUnitsPerCell),
|
||||
Mathf.FloorToInt(worldPos.y / _worldUnitsPerCell));
|
||||
{
|
||||
// R11-N9 减去 _worldOriginOffset 对齐格子坐标系,再除以单格世界尺寸
|
||||
var adjusted = worldPos - _worldOriginOffset;
|
||||
return new(
|
||||
Mathf.FloorToInt(adjusted.x / _worldUnitsPerCell),
|
||||
Mathf.FloorToInt(adjusted.y / _worldUnitsPerCell));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将世界坐标转换为所属房间 ID 和房间内归一化位置(0~1)。
|
||||
/// 实现 <see cref="IPlayerPositionProvider.TryGetRoomAtWorldPos"/>。
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user