多轮审查和修复

This commit is contained in:
2026-05-12 15:34:08 +08:00
parent f55d2a57c3
commit ebbbb7332e
805 changed files with 838724 additions and 1905 deletions

View File

@@ -9,7 +9,9 @@
"rootNamespace": "BaseGames.World.Map",
"references": [
"BaseGames.World",
"BaseGames.Core.Save"
"BaseGames.Core",
"BaseGames.Core.Save",
"BaseGames.Core.Events"
],
"autoReferenced": true,
"overrideReferences": false,

View File

@@ -0,0 +1,14 @@
// Assets/Scripts/World/Map/IMapService.cs
// 地图服务接口,通过 ServiceLocator 注册与查询。
// MapManager 实现此接口MapPanel 等调用方通过接口解耦。
namespace BaseGames.World.Map
{
public interface IMapService
{
bool IsExplored(string roomId);
bool IsMapped(string roomId);
void SetMapped(string roomId);
MapDatabaseSO Database { get; }
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ce82de7829d7e0141b6811c5f70373b0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,96 @@
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
[SerializeField] private StringEventChannelSO _onMapUpdated; // 发布:房间发现时
// 三级可见性:
// Unknown → 未进入过(默认)
// Explored → 进入过(显示轮廓/格子)
// Mapped → 完整地图信息(购买 MapFragment 或存档点揭示)
private HashSet<string> _exploredRooms = new();
private HashSet<string> _mappedRooms = new();
private readonly CompositeDisposable _subs = new();
private void Awake()
{
if (ServiceLocator.GetOrDefault<IMapService>() != null) { Destroy(gameObject); return; }
ServiceLocator.Register<IMapService>(this);
}
private void OnEnable()
{
_onRoomEntered?.Subscribe(OnRoomEntered).AddTo(_subs);
ServiceLocator.GetOrDefault<ISaveableRegistry>()?.Register(this);
}
private void OnDisable()
{
_subs.Clear();
ServiceLocator.GetOrDefault<ISaveableRegistry>()?.Unregister(this);
}
// ── ISaveable ─────────────────────────────────────────────────────────
public void OnSave(SaveData data)
{
data.Map.ExploredRooms ??= new List<string>();
data.Map.ExploredRooms.Clear();
data.Map.ExploredRooms.AddRange(_exploredRooms);
data.Map.MappedRooms ??= new List<string>();
data.Map.MappedRooms.Clear();
data.Map.MappedRooms.AddRange(_mappedRooms);
}
public void OnLoad(SaveData data)
{
_exploredRooms = new HashSet<string>(data.Map.ExploredRooms ?? new System.Collections.Generic.List<string>());
_mappedRooms = new HashSet<string>(data.Map.MappedRooms ?? new System.Collections.Generic.List<string>());
}
// ── 事件驱动房间发现 ──────────────────────────────────────────────────
private void OnRoomEntered(string roomId)
{
bool changed = _exploredRooms.Add(roomId);
if (changed) _onMapUpdated?.Raise(roomId);
}
/// <summary>标记为已完整获取地图信息(购买 MapFragment SO 触发)。</summary>
public void SetMapped(string roomId)
{
_exploredRooms.Add(roomId);
if (_mappedRooms.Add(roomId))
_onMapUpdated?.Raise(roomId);
}
// ── 查询 API ──────────────────────────────────────────────────────────
public bool IsExplored(string roomId) => _exploredRooms.Contains(roomId);
public bool IsMapped(string roomId) => _mappedRooms.Contains(roomId);
public MapDatabaseSO Database => _database;
private void OnDestroy()
{
ServiceLocator.Unregister<IMapService>(this);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6a752b1e0e60f8e41a3b5f7483894d5a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,123 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using BaseGames.Core;
using BaseGames.Core.Events;
namespace BaseGames.World.Map
{
/// <summary>
/// 全屏地图 UI 面板(架构 15_MapShopModule §1.3)。
/// 由 UIManager PanelStack 管理开关OnEnable 时重建格子并订阅更新事件。
/// </summary>
public class MapPanel : MonoBehaviour
{
[SerializeField] private MapDatabaseSO _database;
[SerializeField] private RectTransform _roomContainer; // 格子图放置根节点
[SerializeField] private MapRoomCellUI _cellPrefab; // 地图格子预制
[Header("图标 Sprites")]
[SerializeField] private Sprite _iconSavePoint;
[SerializeField] private Sprite _iconBossRoom;
[SerializeField] private Sprite _iconShop;
[SerializeField] private Sprite _iconPlayerPos;
[Header("颜色")]
[SerializeField] private Color _colorDiscovered = Color.white;
[SerializeField] private Color _colorUndiscovered = Color.black;
[Header("Event Channels")]
[SerializeField] private StringEventChannelSO _onMapUpdated; // 房间发现时刷新
private Dictionary<string, MapRoomCellUI> _cells = new();
private readonly CompositeDisposable _subs = new();
private void OnEnable()
{
BuildGrid();
_onMapUpdated?.Subscribe(OnMapUpdated).AddTo(_subs);
}
private void OnDisable()
{
_subs.Clear();
// 清理动态生成的格子
foreach (var cell in _cells.Values)
if (cell != null) Destroy(cell.gameObject);
_cells.Clear();
}
// ── 内部 ──────────────────────────────────────────────────────────────
private void BuildGrid()
{
if (_database == null || _database.AllRooms == null) return;
var mapManager = ServiceLocator.GetOrDefault<IMapService>();
foreach (var room in _database.AllRooms)
{
if (room == null) continue;
var cell = Instantiate(_cellPrefab, _roomContainer);
bool discovered = mapManager != null && mapManager.IsExplored(room.RoomId);
cell.Setup(room, discovered, ChooseIcon(room));
_cells[room.RoomId] = cell;
}
}
private void OnMapUpdated(string roomId)
{
if (_cells.TryGetValue(roomId, out var cell))
cell.SetDiscovered(true);
}
private Sprite ChooseIcon(MapRoomDataSO room)
{
if (room.MapIconOverride != null) return room.MapIconOverride;
if (room.IsSavePoint) return _iconSavePoint;
if (room.IsBossRoom) return _iconBossRoom;
if (room.IsShop) return _iconShop;
return null;
}
}
// ─── 单个地图格子 UI ─────────────────────────────────────────────────────────
/// <summary>地图面板中每个房间对应的格子 UI 组件。</summary>
public class MapRoomCellUI : MonoBehaviour
{
[SerializeField] private Image _bg;
[SerializeField] private Image _icon;
private static readonly Color Discovered = Color.white;
private static readonly Color Undiscovered = Color.black;
/// <summary>初始化格子(位置、颜色、图标)。</summary>
public void Setup(MapRoomDataSO room, bool discovered, Sprite icon)
{
// 根据 GridPosition/GridSize 设置 RectTransform 位置与大小
if (TryGetComponent<RectTransform>(out var rt))
{
rt.anchoredPosition = new Vector2(
room.GridPosition.x * 32f,
room.GridPosition.y * 32f);
rt.sizeDelta = new Vector2(
room.GridSize.x * 32f,
room.GridSize.y * 32f);
}
SetDiscovered(discovered);
if (_icon != null)
{
_icon.sprite = icon;
_icon.enabled = icon != null;
}
}
public void SetDiscovered(bool v)
{
if (_bg != null) _bg.color = v ? Discovered : Undiscovered;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c9677e71b6b3d4d43aa3680aa8990a83
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,52 @@
using System.Collections.Generic;
using UnityEngine;
using BaseGames.Core;
using BaseGames.Core.Save;
namespace BaseGames.World.Map
{
/// <summary>
/// 地图自定义标记管理器(架构 15_MapShopModule §1.5)。
/// 实现 ISaveable通过 SaveManager 持久化玩家地图标记。
/// MapPin/PinType 数据类定义在 SaveData.csBaseGames.Core.Save避免循环依赖。
/// </summary>
public class MapPinManager : MonoBehaviour, ISaveable
{
private List<MapPin> _pins = new();
public IReadOnlyList<MapPin> Pins => _pins;
private void OnEnable() => ServiceLocator.GetOrDefault<ISaveableRegistry>()?.Register(this);
private void OnDisable() => ServiceLocator.GetOrDefault<ISaveableRegistry>()?.Unregister(this);
// ── 公共 API ──────────────────────────────────────────────────────────
public void AddPin(MapPin pin)
{
if (pin != null) _pins.Add(pin);
}
public void RemovePin(MapPin pin) => _pins.Remove(pin);
/// <summary>便捷方法:用枚举类型创建并添加标记。</summary>
public MapPin CreatePin(string roomId, float normX, float normY,
PinType type = PinType.Marker, string note = "")
{
var pin = new MapPin
{
RoomId = roomId,
NormalizedPosX = normX,
NormalizedPosY = normY,
PinTypeInt = (int)type,
Note = note,
};
AddPin(pin);
return pin;
}
// ── ISaveable ─────────────────────────────────────────────────────────
public void OnSave(SaveData data) => data.Map.Pins = _pins;
public void OnLoad(SaveData data) => _pins = data.Map.Pins ?? new List<MapPin>();
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0d5b16698a16d38428ae6c836d5c4536
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,51 @@
using UnityEngine;
namespace BaseGames.World.Map
{
/// <summary>
/// 将玩家世界坐标转换为地图格子坐标,供 MapPanel 显示玩家位置图标(架构 15_MapShopModule §1.4)。
/// 挂在 Player GameObject 上LateUpdate 每帧计算)。
/// </summary>
public class MapPlayerTracker : MonoBehaviour
{
[SerializeField] private Transform _playerTransform;
[SerializeField] private MapDatabaseSO _database;
[Header("世界坐标 → 格子坐标换算参数")]
[SerializeField] private float _worldUnitsPerCell = 18f; // 1 格 = N 世界单位
/// <summary>玩家当前所在房间 ID用于地图高亮当前房间。</summary>
public string CurrentRoomId { get; private set; }
/// <summary>玩家在当前格子房间内的归一化坐标0~1。</summary>
public Vector2 NormalizedPositionInRoom { get; private set; }
private void LateUpdate()
{
if (_playerTransform == null || _database?.AllRooms == null) return;
Vector2 worldPos = _playerTransform.position;
Vector2Int cellPos = WorldToCell(worldPos);
foreach (var room in _database.AllRooms)
{
if (room == null) continue;
var rect = new RectInt(room.GridPosition, room.GridSize);
if (rect.Contains(cellPos))
{
CurrentRoomId = room.RoomId;
Vector2 inRoom = (Vector2)(cellPos - room.GridPosition);
NormalizedPositionInRoom = new Vector2(
inRoom.x / Mathf.Max(1, room.GridSize.x),
inRoom.y / Mathf.Max(1, room.GridSize.y));
return;
}
}
}
private Vector2Int WorldToCell(Vector2 worldPos)
=> new(
Mathf.FloorToInt(worldPos.x / _worldUnitsPerCell),
Mathf.FloorToInt(worldPos.y / _worldUnitsPerCell));
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3f3715a3378c2004a89bc0ee56ca25c6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace BaseGames.World.Map
{
/// <summary>
/// 单个房间的地图数据 SO架构 15_MapShopModule §1.1)。
/// 资产路径: Assets/ScriptableObjects/Map/Room_{RoomId}.asset
/// </summary>
[CreateAssetMenu(menuName = "World/Map/RoomData")]
public class MapRoomDataSO : ScriptableObject
{
[Header("基础信息")]
public string RoomId; // 与场景名一致,如 "Room_Forest_01"
public string RegionId; // 所属区域,如 "Forest"
public string DisplayName; // 可选,地图 Tooltip
[Header("地图布局(格子坐标,单位:格)")]
public Vector2Int GridPosition; // 左下角坐标
public Vector2Int GridSize; // 宽×高(格)
[Header("房间轮廓纹理")]
public Texture2D RoomOutlineTex; // 用于地图 UI 显示房间形状(可空,回退到矩形格子)
[Header("出口信息")]
public RoomExitData[] Exits; // 该房间所有出口定义
[Header("特殊标记")]
public bool IsBossRoom;
public bool IsSavePoint;
public bool IsShop;
public Sprite MapIconOverride; // null = 按 isXxx 自动选择图标
}
[Serializable]
public struct RoomExitData
{
public string TargetRoomId; // 连接的目标房间 ID
public Vector2Int ExitGridPos; // 出口在格子地图上的位置
public ExitDirection Direction; // 出口方向
}
public enum ExitDirection { Up, Down, Left, Right }
// ─── 全局地图数据库 ──────────────────────────────────────────────────────────
/// <summary>
/// 全局地图数据库 SO编辑器配置一次架构 15_MapShopModule §1.1)。
/// 资产路径: Assets/ScriptableObjects/Map/MapDatabase.asset
/// </summary>
[CreateAssetMenu(menuName = "World/Map/MapDatabase")]
public class MapDatabaseSO : ScriptableObject
{
public MapRoomDataSO[] AllRooms;
private Dictionary<string, MapRoomDataSO> _index;
/// <summary>运行时快速查找(首次调用时建立索引)。</summary>
public MapRoomDataSO GetRoom(string roomId)
{
if (_index == null)
_index = AllRooms.Where(r => r != null)
.ToDictionary(r => r.RoomId);
_index.TryGetValue(roomId, out var r);
return r;
}
private void OnDisable() => _index = null; // SO 卸载时清理缓存
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 00cffb59dd3827e41acf0e7697861b11
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: