// NOTE: 此文件包含 MapPinManager 类,但文件名为 MapPin.cs(历史遗留,Unity .meta 绑定限制不可安全重命名)。 // 如需搜索,请搜索 "MapPinManager" 类名,而非文件名。 // PinSpriteEntry 已迁移到 MapPinConfigSO.cs(R12-N3)。 using System.Collections.Generic; using UnityEngine; using BaseGames.Core; using BaseGames.Core.Save; namespace BaseGames.World.Map { /// /// 地图自定义标记管理器(架构 15_MapShopModule §1.5)。 /// 实现 ,通过 ServiceLocator 对外暴露。 /// /// 每次 Pin 集合变化时自增,外部消费方(MapPanel) /// 可通过版本号判断是否需要重绘,避免无效 Instantiate。 /// /// MapPin/PinType 数据类定义在 SaveData.cs(BaseGames.Core.Save)中,避免循环依赖。 /// [DefaultExecutionOrder(-500)] // 晚于 MapPlayerTracker(-600),早于默认 0,确保 IPinService 在 UI SubscribeServices 前已注册 public class MapPinManager : MonoBehaviour, ISaveable, IPinService { private List _pins = new(); public IReadOnlyList Pins => _pins; /// 每次 Pin 集合发生变化时自增;外部消费方通过此版本号实现脏检查。 public int PinsVersion { get; private set; } private IMapService _mapSvc; // Start 中缓存,避免 CreatePin 每次调用 ServiceLocator private bool _isDuplicate; private void Awake() { if (ServiceLocator.GetOrDefault() != null) { _isDuplicate = true; Destroy(gameObject); return; } // 服务注册迁移到 Awake/OnDestroy(与 MapManager / MapPlayerTracker 对齐) // 避免 OnEnable/OnDisable 模式下反复 Register/Unregister 导致其他模块持有过期引用 ServiceLocator.Register(this); } private void Start() { if (_isDuplicate) return; _mapSvc = ServiceLocator.GetOrDefault(); } private void OnEnable() { if (_isDuplicate) return; ServiceLocator.GetOrDefault()?.Register(this); } private void OnDisable() { if (_isDuplicate) return; ServiceLocator.GetOrDefault()?.Unregister(this); } private void OnDestroy() { if (_isDuplicate) return; ServiceLocator.Unregister(this); } // ── 公共 API ────────────────────────────────────────────────────────── public void AddPin(MapPin pin) { if (pin != null) { _pins.Add(pin); PinsVersion++; } } public void RemovePin(MapPin pin) { if (_pins.Remove(pin)) PinsVersion++; } /// /// 便捷方法:用枚举类型创建并添加标记。 /// /// 参数会做安全校验:roomId 为空时返回 null;normX/normY 自动 Clamp01; /// note 限制 64 字符;可选检查 roomId 是否存在于数据库(不存在时 Warning,但仍允许创建以兼容运行时尚未加载的数据库场景)。 /// /// public MapPin CreatePin(string roomId, float normX, float normY, PinType type = PinType.Marker, string note = "") { if (string.IsNullOrEmpty(roomId)) { Debug.LogWarning("[MapPinManager] CreatePin 失败:roomId 为空。"); return null; } // 可选 RoomId 存在性验证:数据库已就绪时检查,未就绪时跳过(不阻塞) if (_mapSvc?.Database != null && _mapSvc.Database.GetRoom(roomId) == null) Debug.LogWarning($"[MapPinManager] CreatePin: roomId '{roomId}' 在数据库中不存在,标记将不会渲染。"); if (!string.IsNullOrEmpty(note) && note.Length > 64) note = note.Substring(0, 64); var pin = new MapPin { RoomId = roomId, NormalizedPosX = Mathf.Clamp01(normX), NormalizedPosY = Mathf.Clamp01(normY), PinTypeInt = (int)type, Note = note, }; AddPin(pin); return pin; } // ── ISaveable ───────────────────────────────────────────────────────── // 拷贝 List 而非直接共享引用,避免序列化期间 AddPin/RemovePin 修改集合产生并发问题 public void OnSave(SaveData data) => data.Map.Pins = new List(_pins); public void OnLoad(SaveData data) { // R11-N3 防御性拷贝:避免与 SaveData.Map.Pins 共享同一引用, // 防止调用方后续修改 data 时污染 _pins(与 OnSave 的方向对称) _pins = data.Map.Pins != null ? new List(data.Map.Pins) : new List(); PinsVersion++; // 加载存档后通知消费方重绘 } } }