Files
zeling_v2/Assets/_Game/Scripts/World/Map/MapManager.cs
Joywayer f74d7f1877 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.
2026-05-25 23:15:12 +08:00

192 lines
8.3 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 System.Linq;
using UnityEngine;
using BaseGames.Core;
using BaseGames.Core.Events;
using BaseGames.Core.Save;
namespace BaseGames.World.Map
{
/// <summary>
/// 运行时地图管理器(架构 15_MapShopModule §1.2)。
/// 挂在 Persistent 场景 [GameManagers] 下,通过事件驱动记录已探索/已完整地图的房间。
/// 实现 ISaveable 持久化探索进度。
/// </summary>
[DefaultExecutionOrder(-700)]
public class MapManager : MonoBehaviour, ISaveable, IMapService
{
[SerializeField] private MapDatabaseSO _database;
[Header("Event Channels")]
[SerializeField] private StringEventChannelSO _onRoomEntered; // 订阅 EVT_RoomEntered
[Tooltip("房间被探索/标记时广播Explored/Mapped地图 UI 已改用 C# 事件,此通道保留供地图外部系统订阅(如成就、音效)。")]
[SerializeField] private StringEventChannelSO _onMapUpdated; // 发布:房间发现时(地图 UI 内当前无订阅者)
[SerializeField] private StringEventChannelSO _onRegionChanged; // 发布玩家首次进入新区域时EVT_RegionChanged
// 三级可见性:
// Unknown → 未进入过(默认)
// Explored → 进入过(显示轮廓/格子)
// Mapped → 完整地图信息(购买 MapFragment 或存档点揭示)
private HashSet<string> _exploredRooms = new();
private HashSet<string> _mappedRooms = new();
private string _currentRegionId;
private int _totalRoomCount = -1; // -1 = 未缓存OnLoad 后重置
private Dictionary<string, MapRoomDataSO[]> _regionCache; // R11-N6 GetRoomsByRegion 结果缓存
private bool _isDuplicate; // Awake 检测到重复实例时置位OnEnable/OnDisable 提前 return
private readonly CompositeDisposable _subs = new();
private void Awake()
{
if (ServiceLocator.GetOrDefault<IMapService>() != null) { _isDuplicate = true; Destroy(gameObject); return; }
ServiceLocator.Register<IMapService>(this);
}
private void OnEnable()
{
if (_isDuplicate) return;
_onRoomEntered?.Subscribe(OnRoomEntered).AddTo(_subs);
ServiceLocator.GetOrDefault<ISaveableRegistry>()?.Register(this);
}
private void OnDisable()
{
if (_isDuplicate) return;
_subs.Clear();
ServiceLocator.GetOrDefault<ISaveableRegistry>()?.Unregister(this);
}
// ── ISaveable ─────────────────────────────────────────────────────────
public void OnSave(SaveData data)
{
data.Map.ExploredRooms = new HashSet<string>(_exploredRooms);
data.Map.MappedRooms = new HashSet<string>(_mappedRooms);
data.Map.LastRegionId = _currentRegionId;
}
public void OnLoad(SaveData data)
{
_exploredRooms = data.Map.ExploredRooms != null ? new HashSet<string>(data.Map.ExploredRooms) : new HashSet<string>();
_mappedRooms = data.Map.MappedRooms != null ? new HashSet<string>(data.Map.MappedRooms) : new HashSet<string>();
_currentRegionId = data.Map.LastRegionId; // 恢复区域 ID避免读档后首次进房误触发 EVT_RegionChanged
_totalRoomCount = -1; // 强制下次调用 GetExplorationProgress 时重新计数
// 读档后广播UI 仅需轻量刷新(不重建结构);订阅 OnExplorationChanged 的 UI 会 RefreshAllCells
OnExplorationChanged?.Invoke();
}
// ── 事件驱动房间发现 ──────────────────────────────────────────────────
private void OnRoomEntered(string roomId)
{
if (string.IsNullOrEmpty(roomId)) return;
bool changed = _exploredRooms.Add(roomId);
if (changed)
{
_onMapUpdated?.Raise(roomId);
OnExplorationChanged?.Invoke();
}
// 区域变化检测RegionId 非空且与上一次不同时广播 EVT_RegionChanged
var regionId = _database?.GetRoom(roomId)?.RegionId;
if (!string.IsNullOrEmpty(regionId) && regionId != _currentRegionId)
{
_currentRegionId = regionId;
_onRegionChanged?.Raise(regionId);
}
}
/// <summary>
/// 标记为已获取地图碎片信息(购买 MapFragment SO 触发)。
/// 仅写入 _mappedRooms若玩家尚未亲自踏入该房间则显示为灰色轮廓Mapped
/// 亲自踏入后Explored优先级更高显示为白色。
/// </summary>
public void SetMapped(string roomId)
{
if (string.IsNullOrEmpty(roomId)) return;
if (_mappedRooms.Add(roomId))
{
_onMapUpdated?.Raise(roomId);
OnRoomMapped?.Invoke(roomId);
OnExplorationChanged?.Invoke();
}
}
/// <inheritdoc/>
public void SetMappedBatch(IEnumerable<string> roomIds)
{
if (roomIds == null) return;
bool anyAdded = false;
foreach (var roomId in roomIds)
{
if (string.IsNullOrEmpty(roomId)) continue;
if (_mappedRooms.Add(roomId))
{
_onMapUpdated?.Raise(roomId);
OnRoomMapped?.Invoke(roomId);
anyAdded = true;
}
}
if (anyAdded) OnExplorationChanged?.Invoke();
}
// ── 查询 API ──────────────────────────────────────────────────────────
public bool IsExplored(string roomId) => _exploredRooms.Contains(roomId);
public bool IsMapped(string roomId) => _mappedRooms.Contains(roomId);
public string CurrentRegionId => _currentRegionId;
public MapDatabaseSO Database => _database;
public int ExploredRoomCount => _exploredRooms.Count;
public float GetExplorationProgress()
{
if (_database?.AllRooms == null || _database.AllRooms.Length == 0) return 0f;
if (_totalRoomCount < 0)
_totalRoomCount = _database.AllRooms.Count(r => r != null);
return _totalRoomCount > 0 ? Mathf.Clamp01((float)_exploredRooms.Count / _totalRoomCount) : 0f;
}
public MapRoomDataSO[] GetRoomsByRegion(string regionId)
{
if (string.IsNullOrEmpty(regionId) || _database?.AllRooms == null)
return System.Array.Empty<MapRoomDataSO>();
// R11-N6 懒加载缓存,避免每帧 LINQ 扫描全量房间数组
_regionCache ??= new Dictionary<string, MapRoomDataSO[]>();
if (!_regionCache.TryGetValue(regionId, out var cached))
{
cached = _database.AllRooms.Where(r => r != null && r.RegionId == regionId).ToArray();
_regionCache[regionId] = cached;
}
return cached;
}
// ── 数据库热更事件 ────────────────────────────────────────────────────
/// <inheritdoc/>
public event Action OnDatabaseChanged;
/// <inheritdoc/>
public event Action OnExplorationChanged;
/// <inheritdoc/>
public event Action<string> OnRoomMapped;
/// <inheritdoc/>
public void NotifyDatabaseChanged()
{
_database?.InvalidateIndex();
_totalRoomCount = -1;
_regionCache = null; // R11-N6 数据库变更时清空区域缓存
OnDatabaseChanged?.Invoke();
}
private void OnDestroy()
{
if (_isDuplicate) return;
ServiceLocator.Unregister<IMapService>(this);
}
}
}