地图系统
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
"BaseGames.Core.Events",
|
||||
"BaseGames.Input",
|
||||
"BaseGames.Localization",
|
||||
"BaseGames.Player",
|
||||
"Unity.TextMeshPro"
|
||||
],
|
||||
"autoReferenced": true,
|
||||
|
||||
@@ -33,6 +33,24 @@ namespace BaseGames.World.Map
|
||||
/// <summary>返回属于指定区域的所有房间数据;regionId 为空时返回空数组。</summary>
|
||||
MapRoomDataSO[] GetRoomsByRegion(string regionId);
|
||||
|
||||
// ── 定位门控(Locator / 指南针)────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 是否已启用"定位"能力(由指南针类护符解锁)。
|
||||
/// 为 false 时,地图 UI 应隐藏玩家位置点——玩家能看地图但不知自己在哪。
|
||||
/// 由 <see cref="SetLocatorEnabled"/> 控制,跨存档持久化。
|
||||
/// </summary>
|
||||
bool IsLocatorEnabled { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 设置定位能力开关(拾取/装备指南针道具时调用 true)。
|
||||
/// 状态变化时触发 <see cref="OnLocatorChanged"/> 并写入存档。
|
||||
/// </summary>
|
||||
void SetLocatorEnabled(bool enabled);
|
||||
|
||||
/// <summary>定位能力开关变化时触发;地图 UI 据此显示/隐藏玩家位置点。</summary>
|
||||
event Action OnLocatorChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 当地图数据库结构发生变化(房间增删/编辑器热改/运行时热更)时触发。
|
||||
/// MapPanel、MinimapHUD 等 UI 应订阅此事件以执行完整重建。
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace BaseGames.World.Map
|
||||
private HashSet<string> _exploredRooms = new();
|
||||
private HashSet<string> _mappedRooms = new();
|
||||
private string _currentRegionId;
|
||||
private bool _locatorEnabled; // 定位能力(指南针);控制地图玩家点是否显示,存档持久化
|
||||
private int _totalRoomCount = -1; // -1 = 未缓存;OnLoad 后重置
|
||||
private Dictionary<string, MapRoomDataSO[]> _regionCache; // R11-N6 GetRoomsByRegion 结果缓存
|
||||
private bool _isDuplicate; // Awake 检测到重复实例时置位,OnEnable/OnDisable 提前 return
|
||||
@@ -60,9 +61,10 @@ namespace BaseGames.World.Map
|
||||
|
||||
public void OnSave(SaveData data)
|
||||
{
|
||||
data.Map.ExploredRooms = new HashSet<string>(_exploredRooms);
|
||||
data.Map.MappedRooms = new HashSet<string>(_mappedRooms);
|
||||
data.Map.LastRegionId = _currentRegionId;
|
||||
data.Map.ExploredRooms = new HashSet<string>(_exploredRooms);
|
||||
data.Map.MappedRooms = new HashSet<string>(_mappedRooms);
|
||||
data.Map.LastRegionId = _currentRegionId;
|
||||
data.Map.LocatorUnlocked = _locatorEnabled;
|
||||
}
|
||||
|
||||
public void OnLoad(SaveData data)
|
||||
@@ -70,10 +72,13 @@ namespace BaseGames.World.Map
|
||||
_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
|
||||
_locatorEnabled = data.Map.LocatorUnlocked;
|
||||
_totalRoomCount = -1; // 强制下次调用 GetExplorationProgress 时重新计数
|
||||
|
||||
// 读档后广播:UI 仅需轻量刷新(不重建结构);订阅 OnExplorationChanged 的 UI 会 RefreshAllCells
|
||||
OnExplorationChanged?.Invoke();
|
||||
// 定位状态可能随存档变化,通知 UI 显示/隐藏玩家位置点
|
||||
OnLocatorChanged?.Invoke();
|
||||
}
|
||||
|
||||
// ── 事件驱动房间发现 ──────────────────────────────────────────────────
|
||||
@@ -136,6 +141,23 @@ namespace BaseGames.World.Map
|
||||
public bool IsExplored(string roomId) => _exploredRooms.Contains(roomId);
|
||||
public bool IsMapped(string roomId) => _mappedRooms.Contains(roomId);
|
||||
public string CurrentRegionId => _currentRegionId;
|
||||
|
||||
// ── 定位门控(指南针)─────────────────────────────────────────────────
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsLocatorEnabled => _locatorEnabled;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event Action OnLocatorChanged;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void SetLocatorEnabled(bool enabled)
|
||||
{
|
||||
if (_locatorEnabled == enabled) return; // 幂等:状态未变化不广播
|
||||
_locatorEnabled = enabled;
|
||||
OnLocatorChanged?.Invoke();
|
||||
}
|
||||
|
||||
public MapDatabaseSO Database => _database;
|
||||
public int ExploredRoomCount => _exploredRooms.Count;
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ using TMPro;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Core.Save;
|
||||
using BaseGames.Localization;
|
||||
|
||||
namespace BaseGames.World.Map
|
||||
{
|
||||
@@ -42,6 +43,17 @@ namespace BaseGames.World.Map
|
||||
[Header("玩家位置")]
|
||||
[SerializeField] private Image _playerIconImg; // _roomContainer 内的玩家图标
|
||||
|
||||
[Tooltip("勾选后,未启用定位能力(IMapService.IsLocatorEnabled)时隐藏玩家图标与当前房间高亮——\n" +
|
||||
"设计:拿到指南针护符前能看地图但不知自己在哪。\n" +
|
||||
"默认 false 保持现有\"恒显\"行为。与 MinimapHUD._requireLocatorForPlayerDot 对称。")]
|
||||
[SerializeField] private bool _requireLocatorForPlayerIcon;
|
||||
|
||||
[Header("传送选择")]
|
||||
[Tooltip("勾选后,点击地图上\"已解锁且已探索\"的传送站房间会触发 OnTeleportStationSelected,\n" +
|
||||
"由 UI 侧 MapTeleportConfirmController 弹出确认框并调用 ITeleportService.RequestTeleport。\n" +
|
||||
"取消勾选可禁止从全屏地图发起传送(如改为仅在传送站处选择目的地)。")]
|
||||
[SerializeField] private bool _allowTeleportFromMap = true;
|
||||
|
||||
[Header("地图标记")]
|
||||
[SerializeField] private Image _pinPrefab;
|
||||
[SerializeField] private MapPinConfigSO _pinConfig; // R12-N3 集中 PinType→Sprite 映射,替代旧的 PinSpriteEntry[]
|
||||
@@ -74,6 +86,15 @@ namespace BaseGames.World.Map
|
||||
private IMapService _mapSvc;
|
||||
private IPlayerPositionProvider _playerProvider;
|
||||
private IPinService _pinService;
|
||||
private ITeleportService _teleportSvc;
|
||||
|
||||
/// <summary>
|
||||
/// 玩家在地图上点击了一个"可传送"的已解锁站点(参数 RoomId)。
|
||||
/// UI 侧(MapTeleportConfirmController,位于 BaseGames.UI 程序集)订阅此事件,
|
||||
/// 弹出确认框并在确认后调用 ITeleportService.RequestTeleport——
|
||||
/// MapPanel 自身不依赖 UI 程序集,避免与 BaseGames.UI 形成循环引用。
|
||||
/// </summary>
|
||||
public event Action<string> OnTeleportStationSelected;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
@@ -99,6 +120,7 @@ namespace BaseGames.World.Map
|
||||
|
||||
RenderPins();
|
||||
UpdatePlayerIcon();
|
||||
RefreshTeleportMarks(); // 每次打开刷新可传送标记(覆盖在站点新解锁后重开地图的场景)
|
||||
CenterOnCurrentRoom();
|
||||
// R12-N8:移除 _onMapUpdated 订阅,避免与 OnExplorationChanged 双重刷新;
|
||||
// _onMapUpdated 字段保留但标记 HideInInspector,防止旧 Prefab 数据丢失。
|
||||
@@ -144,10 +166,12 @@ namespace BaseGames.World.Map
|
||||
_mapSvc.OnDatabaseChanged += OnDatabaseChanged;
|
||||
_mapSvc.OnExplorationChanged += OnExplorationChanged;
|
||||
_mapSvc.OnRoomMapped += OnRoomMappedAnim;
|
||||
_mapSvc.OnLocatorChanged += OnLocatorChanged;
|
||||
}
|
||||
}
|
||||
_playerProvider ??= ServiceLocator.GetOrDefault<IPlayerPositionProvider>();
|
||||
_pinService ??= ServiceLocator.GetOrDefault<IPinService>();
|
||||
_teleportSvc ??= ServiceLocator.GetOrDefault<ITeleportService>(); // 可选:未注册时地图仍正常工作,仅不显示可传送标记
|
||||
if (_mapSvc != null && _playerProvider != null && _pinService != null)
|
||||
_servicesReady = true;
|
||||
}
|
||||
@@ -162,6 +186,7 @@ namespace BaseGames.World.Map
|
||||
_mapSvc.OnDatabaseChanged -= OnDatabaseChanged;
|
||||
_mapSvc.OnExplorationChanged -= OnExplorationChanged;
|
||||
_mapSvc.OnRoomMapped -= OnRoomMappedAnim;
|
||||
_mapSvc.OnLocatorChanged -= OnLocatorChanged;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,6 +215,13 @@ namespace BaseGames.World.Map
|
||||
RebuildAll();
|
||||
}
|
||||
|
||||
/// <summary>定位能力开关变化:重置 dirty 标记强制 UpdatePlayerIcon 重新评估门控。</summary>
|
||||
private void OnLocatorChanged()
|
||||
{
|
||||
_lastIconRoomId = null; // 绕过 LateUpdate 的 roomId/normPos 脏检查,确保门控状态立即生效
|
||||
if (isActiveAndEnabled) UpdatePlayerIcon();
|
||||
}
|
||||
|
||||
/// <summary>R10-N12 探索进度变化:仅刷新格子可见性,不重建结构(轻量级)。</summary>
|
||||
private void OnExplorationChanged()
|
||||
{
|
||||
@@ -252,6 +284,27 @@ namespace BaseGames.World.Map
|
||||
if (cell == null) continue;
|
||||
cell.SetVisibility(_mapSvc.GetVisibility(roomId));
|
||||
}
|
||||
RefreshTeleportMarks(); // 探索状态影响 CanTeleportTo,同步刷新可传送标记
|
||||
}
|
||||
|
||||
// ── 传送选择 ──────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>房间是否可作为传送目的地:允许从地图传送 + 传送服务存在 + 已解锁且已探索。</summary>
|
||||
private bool IsTeleportable(string roomId)
|
||||
=> _allowTeleportFromMap && _teleportSvc != null && _teleportSvc.CanTeleportTo(roomId);
|
||||
|
||||
/// <summary>格子点击回调:仅对可传送站点转发选择事件,交由 UI 侧确认并执行传送。</summary>
|
||||
private void OnCellClicked(string roomId)
|
||||
{
|
||||
if (IsTeleportable(roomId))
|
||||
OnTeleportStationSelected?.Invoke(roomId);
|
||||
}
|
||||
|
||||
/// <summary>刷新所有格子的"可传送"标记(探索/传送解锁变化、面板重开时调用)。</summary>
|
||||
private void RefreshTeleportMarks()
|
||||
{
|
||||
foreach (var (roomId, cell) in _cells)
|
||||
if (cell != null) cell.SetTeleportable(IsTeleportable(roomId));
|
||||
}
|
||||
|
||||
// ── 格子 & 出口连接 ──────────────────────────────────────────────────
|
||||
@@ -279,6 +332,8 @@ namespace BaseGames.World.Map
|
||||
// R11-N8 布局单独调用 SetGridLayout,与 MinimapHUD.PlaceCell 职责对称
|
||||
cell.SetGridLayout(room, MapGridConstants.FullMapCellPixels);
|
||||
cell.SetColors(_colorExplored, _colorMapped, _colorUnknown);
|
||||
cell.SetClickHandler(OnCellClicked);
|
||||
cell.SetTeleportable(IsTeleportable(room.RoomId));
|
||||
_cells[room.RoomId] = cell;
|
||||
}
|
||||
DrawExits();
|
||||
@@ -343,6 +398,15 @@ namespace BaseGames.World.Map
|
||||
private void UpdatePlayerIcon()
|
||||
{
|
||||
if (_playerIconImg == null || _playerProvider == null) return;
|
||||
|
||||
// 定位门控:未启用定位能力时隐藏玩家图标与当前房间高亮(由指南针护符解锁)
|
||||
if (_requireLocatorForPlayerIcon && (_mapSvc == null || !_mapSvc.IsLocatorEnabled))
|
||||
{
|
||||
_playerIconImg.enabled = false;
|
||||
UpdateCellHighlight(null);
|
||||
return;
|
||||
}
|
||||
|
||||
var roomId = _playerProvider.CurrentRoomId;
|
||||
if (string.IsNullOrEmpty(roomId) || !_cells.TryGetValue(roomId, out var cell))
|
||||
{
|
||||
@@ -469,7 +533,8 @@ namespace BaseGames.World.Map
|
||||
private void ShowTooltip(string text)
|
||||
{
|
||||
if (_tooltipPanel == null || string.IsNullOrEmpty(text)) return;
|
||||
if (_tooltipText != null) _tooltipText.text = text;
|
||||
// 房间名走本地化:text 为本地化 Key 时解析为译文;为普通名称(未命中 Key)时原样显示(向后兼容)。
|
||||
if (_tooltipText != null) _tooltipText.text = LocalizationManager.Get(text, LocalizationTable.UI);
|
||||
_tooltipPanel.SetActive(true);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
namespace BaseGames.World.Map
|
||||
{
|
||||
@@ -29,6 +30,10 @@ namespace BaseGames.World.Map
|
||||
"例如:关卡第一个房间的世界左下角在 (-36, -18) 时,此处填 (-36, -18)。")]
|
||||
[SerializeField] private Vector2 _worldOriginOffset = Vector2.zero;
|
||||
|
||||
[Tooltip("玩家进入新房间时广播此频道(EVT_RoomEntered):MapManager 据此标记已探索、" +
|
||||
"RoomStreamingManager 重算流式集、EventChainManager 触发房间条件。留空则不广播。")]
|
||||
[SerializeField] private StringEventChannelSO _onRoomEntered;
|
||||
|
||||
/// <summary>玩家当前所在房间 ID;未在任何已知房间内时为 null。</summary>
|
||||
public string CurrentRoomId { get; private set; }
|
||||
|
||||
@@ -101,7 +106,10 @@ namespace BaseGames.World.Map
|
||||
_currentRoom = _database.GetRoom(newRoomId);
|
||||
|
||||
if (newRoomId != prevRoomId)
|
||||
{
|
||||
OnRoomChanged?.Invoke(newRoomId);
|
||||
_onRoomEntered?.Raise(newRoomId); // 生产者:进房广播,驱动地图揭示/流式/事件链
|
||||
}
|
||||
}
|
||||
|
||||
// 每帧从世界坐标精确计算归一化位置,实现平滑图标跟随
|
||||
|
||||
@@ -4,6 +4,7 @@ using TMPro;
|
||||
using System.Collections.Generic;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Localization;
|
||||
|
||||
namespace BaseGames.World.Map
|
||||
{
|
||||
@@ -88,7 +89,7 @@ namespace BaseGames.World.Map
|
||||
float progress = _mapSvc.GetExplorationProgress();
|
||||
try
|
||||
{
|
||||
_globalProgressText.text = string.Format(_globalFormat, progress);
|
||||
_globalProgressText.text = string.Format(ResolveFormat(_globalFormat, "{0:P0}"), progress);
|
||||
}
|
||||
catch (System.FormatException)
|
||||
{
|
||||
@@ -111,7 +112,7 @@ namespace BaseGames.World.Map
|
||||
string regionDisplayName = ResolveRegionDisplayName(_currentRegionId);
|
||||
try
|
||||
{
|
||||
_regionProgressText.text = string.Format(_regionFormat, regionProgress, regionDisplayName);
|
||||
_regionProgressText.text = string.Format(ResolveFormat(_regionFormat, "{1} {0:P0}"), regionProgress, regionDisplayName);
|
||||
}
|
||||
catch (System.FormatException)
|
||||
{
|
||||
@@ -124,6 +125,21 @@ namespace BaseGames.World.Map
|
||||
|
||||
// ── 辅助方法 ──────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 将格式串字段解析为本地化格式串,并保证始终含 {0} 占位符:
|
||||
/// ① 字段填本地化 Key(如 "MAP_PROGRESS_GLOBAL")→ 返回本地化值(如 "已探索 {0:P0}");
|
||||
/// ② 字段直接是格式串 → Get 原样返回(向后兼容);
|
||||
/// ③ 本地化服务缺失 / Key 未命中(返回值无 {0})→ 回退到 <paramref name="fallback"/>,
|
||||
/// 确保即使本地化未就绪也不丢失百分比数值。
|
||||
/// </summary>
|
||||
private static string ResolveFormat(string keyOrFormat, string fallback)
|
||||
{
|
||||
if (string.IsNullOrEmpty(keyOrFormat)) return fallback;
|
||||
string s = LocalizationManager.Get(keyOrFormat, LocalizationTable.UI);
|
||||
// 检测 "{0"(兼容 "{0}"、"{0:P0}"、"{0:F1}" 等格式说明符);缺占位符视为未解析,用回退
|
||||
return (!string.IsNullOrEmpty(s) && s.Contains("{0")) ? s : fallback;
|
||||
}
|
||||
|
||||
/// <summary>预建 RegionId → Entry 字典,O(1) 查询。</summary>
|
||||
private void BuildRegionDict()
|
||||
=> _regionDict = MapServiceExtensions.BuildRegionDict(_regionNames);
|
||||
|
||||
@@ -15,13 +15,14 @@ namespace BaseGames.World.Map
|
||||
/// 颜色通过 <see cref="SetColors"/> 从外部注入,不在此处硬编码。
|
||||
/// <para><see cref="RT"/> 属性在 Awake 中缓存,避免调用方反复 GetComponent。</para>
|
||||
/// </summary>
|
||||
public class MapRoomCellUI : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler
|
||||
public class MapRoomCellUI : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler, IPointerClickHandler
|
||||
{
|
||||
[SerializeField] private Image _bg;
|
||||
[SerializeField] private Image _icon;
|
||||
[SerializeField] private RawImage _outlineImage; // 可选:房间非矩形轮廓纹理
|
||||
[SerializeField] private Image _highlight; // 可选:当前房间高亮描边(玩家所在时激活)
|
||||
[SerializeField] private Image _fogOverlay; // 可选:未知房间雾效覆盖层(R12-FD)
|
||||
[SerializeField] private Image _teleportMarker;// 可选:可传送站点标记(CanTeleportTo 为真时激活)
|
||||
|
||||
// 实例颜色(默认值与原硬编码保持一致);可通过 SetColors 统一覆盖
|
||||
private Color _colExplored = Color.white;
|
||||
@@ -30,8 +31,10 @@ namespace BaseGames.World.Map
|
||||
private RoomVisibility _currentVisibility;
|
||||
|
||||
private string _displayName;
|
||||
private string _roomId; // 供点击回调携带(传送选择)
|
||||
private Action<string> _onHover;
|
||||
private Action _onHoverExit;
|
||||
private Action<string> _onClick; // 点击回调(传送选择),由 MapPanel 注入
|
||||
|
||||
/// <summary>格子的 RectTransform(Awake 中缓存,外部直接访问无需 GetComponent)。</summary>
|
||||
public RectTransform RT { get; private set; }
|
||||
@@ -47,6 +50,7 @@ namespace BaseGames.World.Map
|
||||
Action<string> onHover = null, Action onHoverExit = null)
|
||||
{
|
||||
_displayName = room.DisplayName;
|
||||
_roomId = room.RoomId;
|
||||
_onHover = onHover;
|
||||
_onHoverExit = onHoverExit;
|
||||
|
||||
@@ -113,6 +117,15 @@ namespace BaseGames.World.Map
|
||||
if (_highlight != null) _highlight.enabled = v;
|
||||
}
|
||||
|
||||
/// <summary>注入点击回调(携带 RoomId),由 MapPanel 在创建格子时设置;MinimapHUD 不设置即不可点击。</summary>
|
||||
public void SetClickHandler(Action<string> onClick) => _onClick = onClick;
|
||||
|
||||
/// <summary>设置"可传送站点"标记显隐(CanTeleportTo 为真时由 MapPanel 调用)。</summary>
|
||||
public void SetTeleportable(bool v)
|
||||
{
|
||||
if (_teleportMarker != null) _teleportMarker.enabled = v;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新发现房间时播放闪白淡出动画(R12-FC)。
|
||||
/// 由 MapPanel.OnRoomMappedAnim 调用;协程安全:组件被销毁后 Unity 自动终止。
|
||||
@@ -138,5 +151,8 @@ namespace BaseGames.World.Map
|
||||
}
|
||||
|
||||
public void OnPointerExit(PointerEventData _) => _onHoverExit?.Invoke();
|
||||
|
||||
/// <summary>点击格子:转发 RoomId 给注入的回调(MapPanel 据 CanTeleportTo 决定是否发起传送选择)。</summary>
|
||||
public void OnPointerClick(PointerEventData _) => _onClick?.Invoke(_roomId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,12 @@ namespace BaseGames.World.Map
|
||||
[SerializeField, Min(1)] private int _viewRadiusCells = 3; // 以玩家房间中心为圆心的可视半径(格)
|
||||
[SerializeField] private float _cellPixels = 16f; // 每格显示像素数
|
||||
|
||||
[Header("定位门控(指南针)")]
|
||||
[Tooltip("勾选后,未启用定位能力(IMapService.IsLocatorEnabled)时隐藏玩家位置点——\n" +
|
||||
"设计:拿到指南针护符前能看地图但不知自己在哪。\n" +
|
||||
"默认 false 保持现有\"恒显\"行为,需要该玩法时再开启。")]
|
||||
[SerializeField] private bool _requireLocatorForPlayerDot;
|
||||
|
||||
[Header("颜色(Inspector 覆盖)")]
|
||||
[SerializeField] private Color _colorExplored = Color.white;
|
||||
[SerializeField] private Color _colorMapped = new Color(0.45f, 0.45f, 0.45f, 1f);
|
||||
@@ -53,6 +59,12 @@ namespace BaseGames.World.Map
|
||||
[SerializeField] private int[] _zoomLevels = { 2, 3, 5 }; // R12-FA 可用视野半径档位(格)
|
||||
private int _zoomLevelIndex;
|
||||
|
||||
[Header("自动贴边(可选)")]
|
||||
[Tooltip("勾选后,启用时把小地图定位到所在 Canvas 的右上角(按 _screenMargin 像素留边)。\n" +
|
||||
"用于父节点非 RectTransform(如普通 Transform 的 HUDRoot)时锚定失效的情况,保证运行时正确贴边。")]
|
||||
[SerializeField] private bool _autoAnchorTopRight = true;
|
||||
[SerializeField] private Vector2 _screenMargin = new Vector2(16f, 16f);
|
||||
|
||||
private IMapService _mapSvc;
|
||||
private IPlayerPositionProvider _playerProvider;
|
||||
private IPinService _pinService;
|
||||
@@ -90,6 +102,8 @@ namespace BaseGames.World.Map
|
||||
// 启动顺序兜底
|
||||
SubscribeServices();
|
||||
|
||||
if (_autoAnchorTopRight) AnchorToCanvasTopRight();
|
||||
|
||||
// R10-N1/N2 应用关闭期间累积的状态变化
|
||||
if (_databaseDirty)
|
||||
{
|
||||
@@ -142,6 +156,7 @@ namespace BaseGames.World.Map
|
||||
{
|
||||
_mapSvc.OnDatabaseChanged += OnDatabaseChanged;
|
||||
_mapSvc.OnExplorationChanged += OnExplorationChanged;
|
||||
_mapSvc.OnLocatorChanged += OnLocatorChanged;
|
||||
}
|
||||
}
|
||||
_pinService ??= ServiceLocator.GetOrDefault<IPinService>();
|
||||
@@ -163,6 +178,7 @@ namespace BaseGames.World.Map
|
||||
{
|
||||
_mapSvc.OnDatabaseChanged -= OnDatabaseChanged;
|
||||
_mapSvc.OnExplorationChanged -= OnExplorationChanged;
|
||||
_mapSvc.OnLocatorChanged -= OnLocatorChanged;
|
||||
_mapSvc = null;
|
||||
}
|
||||
_pinService = null;
|
||||
@@ -219,6 +235,13 @@ namespace BaseGames.World.Map
|
||||
if (cell != null) cell.SetVisibility(_mapSvc.GetVisibility(id));
|
||||
}
|
||||
|
||||
/// <summary>定位能力开关变化:重置 dirty 标记强制 UpdatePlayerDot 重新评估门控。</summary>
|
||||
private void OnLocatorChanged()
|
||||
{
|
||||
_lastDotRoomId = null; // 绕过 UpdatePlayerDot 的 roomId/normPos 脏检查,确保门控状态立即生效
|
||||
if (isActiveAndEnabled) UpdatePlayerDot();
|
||||
}
|
||||
|
||||
/// <summary>数据库结构变更时完整重建:清空所有格子,下次 RefreshView 重新实例化。</summary>
|
||||
private void OnDatabaseChanged()
|
||||
{
|
||||
@@ -386,6 +409,13 @@ namespace BaseGames.World.Map
|
||||
{
|
||||
if (_playerDot == null || _playerProvider == null) return;
|
||||
|
||||
// 定位门控:未启用定位能力时隐藏玩家点(由指南针护符解锁)
|
||||
if (_requireLocatorForPlayerDot && (_mapSvc == null || !_mapSvc.IsLocatorEnabled))
|
||||
{
|
||||
_playerDot.enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var roomId = _playerProvider.CurrentRoomId;
|
||||
var normPos = _playerProvider.NormalizedPositionInRoom;
|
||||
|
||||
@@ -429,5 +459,26 @@ namespace BaseGames.World.Map
|
||||
_viewDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 自动贴边 ──────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 将小地图定位到所在 Canvas 的右上角(pivot 设为右上,position 设到画布右上角内缩 _screenMargin)。
|
||||
/// 不依赖父节点为 RectTransform——直接用世界坐标定位,规避父节点为普通 Transform 时锚定失效的问题。
|
||||
/// </summary>
|
||||
private void AnchorToCanvasTopRight()
|
||||
{
|
||||
var canvas = GetComponentInParent<Canvas>();
|
||||
if (canvas == null) return;
|
||||
if (canvas.transform is not RectTransform canvasRt) return;
|
||||
if (transform is not RectTransform rt) return;
|
||||
|
||||
var corners = new Vector3[4];
|
||||
canvasRt.GetWorldCorners(corners); // 0=BL, 1=TL, 2=TR, 3=BR
|
||||
rt.pivot = new Vector2(1f, 1f);
|
||||
rt.position = new Vector3(corners[2].x - _screenMargin.x,
|
||||
corners[2].y - _screenMargin.y,
|
||||
rt.position.z);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
40
Assets/_Game/Scripts/World/Map/RegionDefinitionSO.cs
Normal file
40
Assets/_Game/Scripts/World/Map/RegionDefinitionSO.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Player;
|
||||
|
||||
namespace BaseGames.World.Map
|
||||
{
|
||||
/// <summary>
|
||||
/// 区域定义 SO。集中管理区域元数据:Identity、地图展示、存档槽背景图、解锁条件。
|
||||
///
|
||||
/// 房间归属关系由 <see cref="MapRoomDataSO.RegionId"/> 维护(单一权威来源)。
|
||||
/// <see cref="RegionRegistrySO"/> 运行时从 <see cref="MapDatabaseSO"/> 构建 SceneName → Region 缓存,
|
||||
/// 不再在此 SO 维护 roomSceneNames 冗余字段。
|
||||
///
|
||||
/// 资产路径: Assets/_Game/Data/Map/Regions/Region_{regionId}.asset
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "BaseGames/Map/RegionDefinition", fileName = "Region_")]
|
||||
public class RegionDefinitionSO : ScriptableObject
|
||||
{
|
||||
[Header("Identity")]
|
||||
public string regionId;
|
||||
public string displayName;
|
||||
|
||||
[Header("Map")]
|
||||
public Color mapColor;
|
||||
public Sprite mapIconSprite;
|
||||
|
||||
[Header("存档槽展示")]
|
||||
[Tooltip("存档槽卡片背景图。建议 480×270,放于 Assets/_Game/Art/UI/SaveSlot/ 下,命名 SaveSlot_BG_{regionId}.png。")]
|
||||
public Sprite saveSlotBackground;
|
||||
|
||||
[Header("解锁条件")]
|
||||
[Tooltip("击败指定 Boss 后解锁此区域;留空 = 无条件。")]
|
||||
public string requiredBossDefeated;
|
||||
[Tooltip("需持有指定能力方可进入;None = 无要求。")]
|
||||
public AbilityType requiredAbility;
|
||||
|
||||
[Header("导航")]
|
||||
[Tooltip("从区域外部进入时的首个房间场景名(RoomId)。")]
|
||||
public string entrySceneName;
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/World/Map/RegionDefinitionSO.cs.meta
Normal file
11
Assets/_Game/Scripts/World/Map/RegionDefinitionSO.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bfbb6453efee45c4fbe6f3b6b2cd463b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
76
Assets/_Game/Scripts/World/Map/RegionRegistrySO.cs
Normal file
76
Assets/_Game/Scripts/World/Map/RegionRegistrySO.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.World.Map
|
||||
{
|
||||
/// <summary>
|
||||
/// 全局区域注册表 SO。
|
||||
///
|
||||
/// 设计原则:<see cref="MapRoomDataSO.RegionId"/> 是房间归属的单一权威来源。
|
||||
/// 本 SO 持有所有 <see cref="RegionDefinitionSO"/> 资产引用,运行时从
|
||||
/// <see cref="MapDatabaseSO"/> 惰性构建 SceneName → Region 缓存,
|
||||
/// 消除了原 RegionDefinitionSO.roomSceneNames 与 MapRoomDataSO.RegionId 的双重冗余。
|
||||
///
|
||||
/// 资产路径:Assets/_Game/Data/Map/RegionRegistry.asset
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "BaseGames/Map/RegionRegistry", fileName = "RegionRegistry")]
|
||||
public class RegionRegistrySO : ScriptableObject
|
||||
{
|
||||
[Tooltip("项目中所有 RegionDefinitionSO 资产。新增区域 SO 时同步添加到此数组。")]
|
||||
[SerializeField] private RegionDefinitionSO[] _regions;
|
||||
|
||||
[Tooltip("地图数据库,用于从 MapRoomDataSO.RegionId 构建 SceneName → Region 缓存(单一权威来源)。")]
|
||||
[SerializeField] private MapDatabaseSO _mapDatabase;
|
||||
|
||||
private Dictionary<string, RegionDefinitionSO> _regionById;
|
||||
private Dictionary<string, RegionDefinitionSO> _sceneToRegion;
|
||||
|
||||
/// <summary>根据场景名(MapRoomDataSO.RoomId)查找所属区域;未找到返回 null。</summary>
|
||||
public RegionDefinitionSO FindBySceneName(string sceneName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(sceneName)) return null;
|
||||
BuildCacheIfNeeded();
|
||||
_sceneToRegion.TryGetValue(sceneName, out var region);
|
||||
return region;
|
||||
}
|
||||
|
||||
/// <summary>根据 regionId 直接查找;未找到返回 null。</summary>
|
||||
public RegionDefinitionSO FindById(string regionId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(regionId)) return null;
|
||||
BuildCacheIfNeeded();
|
||||
_regionById.TryGetValue(regionId, out var region);
|
||||
return region;
|
||||
}
|
||||
|
||||
private void BuildCacheIfNeeded()
|
||||
{
|
||||
if (_sceneToRegion != null) return;
|
||||
|
||||
// Step 1: regionId → RegionDefinitionSO
|
||||
_regionById = new Dictionary<string, RegionDefinitionSO>(System.StringComparer.OrdinalIgnoreCase);
|
||||
if (_regions != null)
|
||||
foreach (var r in _regions)
|
||||
if (r != null && !string.IsNullOrEmpty(r.regionId))
|
||||
_regionById[r.regionId] = r;
|
||||
|
||||
// Step 2: sceneName → RegionDefinitionSO,以 MapRoomDataSO.RegionId 为权威来源
|
||||
_sceneToRegion = new Dictionary<string, RegionDefinitionSO>(System.StringComparer.OrdinalIgnoreCase);
|
||||
if (_mapDatabase?.AllRooms == null) return;
|
||||
|
||||
foreach (var room in _mapDatabase.AllRooms)
|
||||
{
|
||||
if (room == null || string.IsNullOrEmpty(room.RoomId) || string.IsNullOrEmpty(room.RegionId))
|
||||
continue;
|
||||
if (_regionById.TryGetValue(room.RegionId, out var def))
|
||||
_sceneToRegion[room.RoomId] = def;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
_sceneToRegion = null;
|
||||
_regionById = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/World/Map/RegionRegistrySO.cs.meta
Normal file
11
Assets/_Game/Scripts/World/Map/RegionRegistrySO.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b8854f90900839747b904fcd1515774d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Core.Save;
|
||||
|
||||
namespace BaseGames.World.Map
|
||||
@@ -22,8 +23,13 @@ namespace BaseGames.World.Map
|
||||
|
||||
private IMapService _mapSvc;
|
||||
private IPlayerPositionProvider _playerProvider;
|
||||
private ISceneService _sceneSvc;
|
||||
private bool _isDuplicate;
|
||||
|
||||
// 传送进行中状态:防止重入,并在玩家到达目标房间后触发 NotifyTeleportCompleted
|
||||
private bool _teleportInProgress;
|
||||
private string _pendingTargetRoomId;
|
||||
|
||||
// ── ITeleportService 事件 ─────────────────────────────────────────────
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -67,6 +73,9 @@ namespace BaseGames.World.Map
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_isDuplicate) return;
|
||||
// 传送途中销毁(如场景重建)时解绑,避免悬挂订阅
|
||||
if (_playerProvider != null)
|
||||
_playerProvider.OnRoomChanged -= OnArrivedRoomChanged;
|
||||
ServiceLocator.Unregister<ITeleportService>(this);
|
||||
}
|
||||
|
||||
@@ -91,10 +100,63 @@ namespace BaseGames.World.Map
|
||||
Debug.LogWarning($"[TeleportService] 无法传送到 '{targetRoomId}':未解锁或未探索。");
|
||||
return;
|
||||
}
|
||||
if (_teleportInProgress)
|
||||
{
|
||||
Debug.LogWarning("[TeleportService] 传送进行中,忽略新的传送请求。");
|
||||
return;
|
||||
}
|
||||
|
||||
_playerProvider ??= ServiceLocator.GetOrDefault<IPlayerPositionProvider>();
|
||||
// 使用当前房间 ID 作为传送来源(与接口参数语义一致)
|
||||
string sourceRoomId = _playerProvider?.CurrentRoomId ?? string.Empty;
|
||||
|
||||
// 通知 UI 播放关闭/淡出动画(地图面板、HUD 等)
|
||||
OnTeleportRequested?.Invoke(sourceRoomId, targetRoomId);
|
||||
|
||||
// 传送到当前所在房间:无需加载场景,直接收尾
|
||||
if (targetRoomId == sourceRoomId)
|
||||
{
|
||||
NotifyTeleportCompleted(targetRoomId);
|
||||
return;
|
||||
}
|
||||
|
||||
// 通过通用场景过渡入口执行实际加载(Scene 类型 = 完整淡出 + 加载画面)。
|
||||
// 流式系统已注册时,SceneService 会委托 ISceneLoadCoordinator 走房间生命周期。
|
||||
_sceneSvc ??= ServiceLocator.GetOrDefault<ISceneService>();
|
||||
if (_sceneSvc == null)
|
||||
{
|
||||
Debug.LogWarning("[TeleportService] ISceneService 未注册,无法执行传送场景加载。");
|
||||
return;
|
||||
}
|
||||
|
||||
_pendingTargetRoomId = targetRoomId;
|
||||
_teleportInProgress = true;
|
||||
|
||||
// 监听玩家到达目标房间 → 触发 NotifyTeleportCompleted(一次性,OnArrivedRoomChanged 内解绑)
|
||||
if (_playerProvider != null)
|
||||
_playerProvider.OnRoomChanged += OnArrivedRoomChanged;
|
||||
|
||||
_sceneSvc.RequestTransition(new SceneLoadRequest
|
||||
{
|
||||
SceneName = targetRoomId, // RoomId 即场景 Addressable key("Room_" 前缀,见 ISceneLoadCoordinator)
|
||||
EntryTransitionId = null, // 默认出生点;精确落到传送站点为后续增强项
|
||||
TransitionType = TransitionType.Scene, // 完整淡出 + 加载画面,符合快速旅行演出
|
||||
ShowLoadingScreen = true,
|
||||
IsRespawn = false,
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 传送途中监听玩家房间变化:到达目标房间后解绑并触发 <see cref="OnTeleportCompleted"/>。
|
||||
/// </summary>
|
||||
private void OnArrivedRoomChanged(string arrivedRoomId)
|
||||
{
|
||||
if (arrivedRoomId != _pendingTargetRoomId) return; // 仅在到达目标房间时收尾
|
||||
if (_playerProvider != null)
|
||||
_playerProvider.OnRoomChanged -= OnArrivedRoomChanged;
|
||||
_teleportInProgress = false;
|
||||
_pendingTargetRoomId = null;
|
||||
NotifyTeleportCompleted(arrivedRoomId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
108
Assets/_Game/Scripts/World/Map/TeleportStationTrigger.cs
Normal file
108
Assets/_Game/Scripts/World/Map/TeleportStationTrigger.cs
Normal file
@@ -0,0 +1,108 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Player;
|
||||
using BaseGames.World;
|
||||
|
||||
namespace BaseGames.World.Map
|
||||
{
|
||||
/// <summary>
|
||||
/// 世界中的传送站/驿站交互点(快速旅行网络节点)。
|
||||
/// <para>
|
||||
/// 玩家交互时:① 通过 <see cref="ITeleportService.UnlockTeleportStation"/> 将本房间注册为
|
||||
/// 快速旅行节点(持久化到存档);② 若玩家已解锁 <see cref="AbilityType.FastTravel"/>,
|
||||
/// 则触发 <see cref="_onTravelMenuRequested"/>(由设计师在 Inspector 中接到地图/传送选择 UI);
|
||||
/// 否则触发 <see cref="_onActivatedWithoutTravel"/>(如显示"快速旅行网络尚未激活"提示)。
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// 本组件只负责"解锁节点 + 请求打开 UI",不持有具体 UI 引用,保持架构解耦——
|
||||
/// 实际传送由 <see cref="ITeleportService.RequestTeleport"/> 在目的地选择 UI 中调用。
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(Collider2D))]
|
||||
public class TeleportStationTrigger : MonoBehaviour, IInteractable
|
||||
{
|
||||
[Header("传送站标识")]
|
||||
[Tooltip("本传送站所属房间 ID(应与场景名/MapRoomDataSO.RoomId 一致)。\n" +
|
||||
"留空时回退到 IPlayerPositionProvider.CurrentRoomId(玩家当前所在房间)。")]
|
||||
[SerializeField] private string _stationRoomId;
|
||||
|
||||
[Tooltip("交互提示文字(互动系统读取)。")]
|
||||
[SerializeField] private string _interactPrompt = "使用传送站";
|
||||
|
||||
[Header("能力门控")]
|
||||
[Tooltip("勾选后,仅当玩家已解锁 FastTravel 能力时才触发传送菜单;\n" +
|
||||
"节点的\"解锁/注册\"不受此限制(发现即记录:先点亮站点、后接通网络)。")]
|
||||
[SerializeField] private bool _requireFastTravelAbility = true;
|
||||
|
||||
[Header("回调(接入 UI / 演出)")]
|
||||
[Tooltip("成功解锁本站且满足能力门控时触发——接到\"打开地图/传送选择 UI\"。")]
|
||||
[SerializeField] private UnityEvent _onTravelMenuRequested;
|
||||
|
||||
[Tooltip("已解锁本站但快速旅行能力未解锁时触发——接到提示文字/音效等。")]
|
||||
[SerializeField] private UnityEvent _onActivatedWithoutTravel;
|
||||
|
||||
// ── IInteractable ─────────────────────────────────────────────────────
|
||||
|
||||
public bool CanInteract => true;
|
||||
public string InteractPrompt => _interactPrompt;
|
||||
|
||||
public void Interact(Transform player)
|
||||
{
|
||||
var teleportSvc = ServiceLocator.GetOrDefault<ITeleportService>();
|
||||
if (teleportSvc == null)
|
||||
{
|
||||
Debug.LogWarning("[TeleportStationTrigger] ITeleportService 未注册,无法解锁传送站。", this);
|
||||
return;
|
||||
}
|
||||
|
||||
string roomId = ResolveRoomId();
|
||||
if (string.IsNullOrEmpty(roomId))
|
||||
{
|
||||
Debug.LogWarning("[TeleportStationTrigger] 无法解析传送站房间 ID(未配置且玩家不在已知房间内)。", this);
|
||||
return;
|
||||
}
|
||||
|
||||
// 发现即注册:解锁节点并持久化(幂等,重复激活无副作用)
|
||||
teleportSvc.UnlockTeleportStation(roomId);
|
||||
|
||||
// 能力门控:决定是否打开传送选择菜单
|
||||
if (_requireFastTravelAbility && !PlayerHasFastTravel(player))
|
||||
{
|
||||
_onActivatedWithoutTravel?.Invoke();
|
||||
return;
|
||||
}
|
||||
|
||||
_onTravelMenuRequested?.Invoke();
|
||||
}
|
||||
|
||||
public void OnPlayerEnterRange(Transform player) { }
|
||||
public void OnPlayerExitRange() { }
|
||||
|
||||
// ── 辅助 ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>解析本站房间 ID:优先序列化字段,回退到玩家当前房间。</summary>
|
||||
private string ResolveRoomId()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_stationRoomId)) return _stationRoomId;
|
||||
return ServiceLocator.GetOrDefault<IPlayerPositionProvider>()?.CurrentRoomId;
|
||||
}
|
||||
|
||||
/// <summary>检查玩家是否已解锁快速旅行能力(从玩家 Transform 取 PlayerStats)。</summary>
|
||||
private static bool PlayerHasFastTravel(Transform player)
|
||||
{
|
||||
if (player == null) return false;
|
||||
var stats = player.GetComponentInParent<PlayerStats>();
|
||||
// PlayerStats 不可用时 Fail-Open:放行打开菜单,避免因引用缺失卡死交互
|
||||
return stats == null || stats.HasAbility(AbilityType.FastTravel);
|
||||
}
|
||||
|
||||
private void OnDrawGizmos()
|
||||
{
|
||||
var col = GetComponent<Collider2D>();
|
||||
if (col == null) return;
|
||||
Gizmos.color = new Color(0.3f, 0.7f, 1f, 0.6f); // 浅蓝:传送站
|
||||
Gizmos.DrawWireCube(transform.position, col.bounds.size);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 85d9028e7948476449c679941ce8baf2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -5,6 +5,7 @@ using UnityEngine.AddressableAssets;
|
||||
using UnityEngine.ResourceManagement.AsyncOperations;
|
||||
using UnityEngine.ResourceManagement.ResourceProviders;
|
||||
using UnityEngine.SceneManagement;
|
||||
using BaseGames.Core.Assets;
|
||||
|
||||
namespace BaseGames.World.Streaming
|
||||
{
|
||||
@@ -74,7 +75,7 @@ namespace BaseGames.World.Streaming
|
||||
|
||||
State = RoomState.Loading;
|
||||
|
||||
_sceneHandle = Addressables.LoadSceneAsync(addressableKey, LoadSceneMode.Additive);
|
||||
_sceneHandle = AssetLoader.LoadSceneAsync(addressableKey, LoadSceneMode.Additive);
|
||||
|
||||
while (!_sceneHandle.IsDone)
|
||||
yield return null;
|
||||
@@ -112,7 +113,7 @@ namespace BaseGames.World.Streaming
|
||||
|
||||
State = RoomState.Unloading;
|
||||
|
||||
var op = Addressables.UnloadSceneAsync(_sceneHandle);
|
||||
var op = AssetLoader.UnloadSceneAsync(_sceneHandle);
|
||||
yield return op;
|
||||
|
||||
_renderers = null;
|
||||
|
||||
@@ -77,9 +77,9 @@ namespace BaseGames.World.Streaming
|
||||
{
|
||||
var graph = new WorldGraph(unitsPerGrid);
|
||||
|
||||
if (database == null || database.AllRooms == null)
|
||||
if (database == null || database.AllRooms == null || database.AllRooms.Length == 0)
|
||||
{
|
||||
Debug.LogError("[WorldGraph] MapDatabaseSO 为空,无法构建世界图。");
|
||||
Debug.LogWarning("[WorldGraph] MapDatabaseSO 为空或无房间数据,跳过世界图构建(新建项目正常)。");
|
||||
return graph;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user