- 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.
316 lines
14 KiB
C#
316 lines
14 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using UnityEngine;
|
||
using UnityEngine.Serialization;
|
||
using BaseGames.Core.Events;
|
||
|
||
namespace BaseGames.World.Map
|
||
{
|
||
/// <summary>
|
||
/// 房间功能类型标记(可多选)。
|
||
/// 替代原先的 IsBossRoom / IsSavePoint / IsShop 三个独立 bool,
|
||
/// 支持复合类型(如一个房间既是存档点也是商店),并便于扩展新类型。
|
||
/// </summary>
|
||
[System.Flags]
|
||
public enum RoomType
|
||
{
|
||
None = 0,
|
||
BossRoom = 1 << 0,
|
||
SavePoint = 1 << 1,
|
||
Shop = 1 << 2,
|
||
Merchant = 1 << 3,
|
||
Challenge = 1 << 4,
|
||
TeleportStation = 1 << 5,
|
||
}
|
||
|
||
/// <summary>
|
||
/// 单个房间的地图数据 SO(架构 15_MapShopModule §1.1)。
|
||
/// 资产路径: Assets/_Game/Data/Map/Rooms/Room_{RoomId}.asset
|
||
/// </summary>
|
||
[CreateAssetMenu(menuName = "BaseGames/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 RoomType RoomFlags; // 支持多类型组合,替代旧的三个 bool 字段
|
||
[HideInInspector] public bool IsBossRoom; // 旧字段,保留序列化兼容性;OnValidate 自动迁移到 RoomFlags
|
||
[HideInInspector] public bool IsSavePoint; // 旧字段,保留序列化兼容性;OnValidate 自动迁移到 RoomFlags
|
||
[HideInInspector] public bool IsShop; // 旧字段,保留序列化兼容性;OnValidate 自动迁移到 RoomFlags
|
||
public Sprite MapIconOverride; // null = 按 RoomFlags 自动选择图标
|
||
|
||
/// <summary>
|
||
/// R20-N2 集中图标优先级逻辑,替代 MapPanel / MinimapHUD 各自重复的 ChooseIcon 实现。
|
||
/// 优先级:MapIconOverride > SavePoint > BossRoom > Shop > TeleportStation。
|
||
/// 对应 Sprite 未配置时返回 null(格子不显示图标)。
|
||
/// </summary>
|
||
public Sprite ChooseDisplayIcon(Sprite savePoint, Sprite boss, Sprite shop, Sprite teleport)
|
||
{
|
||
if (MapIconOverride != null) return MapIconOverride;
|
||
if (RoomFlags.HasFlag(RoomType.SavePoint) || IsSavePoint) return savePoint;
|
||
if (RoomFlags.HasFlag(RoomType.BossRoom) || IsBossRoom) return boss;
|
||
if (RoomFlags.HasFlag(RoomType.Shop) || IsShop) return shop;
|
||
if (RoomFlags.HasFlag(RoomType.TeleportStation)) return teleport;
|
||
return null;
|
||
}
|
||
|
||
[Header("流式加载")]
|
||
[Tooltip("此房间场景资产的预估内存(KB)。\n" +
|
||
"在 Profiler 中测量场景实际内存后填入,供流式管理器执行内存预算检查使用。\n" +
|
||
"建议在关卡内容基本定型后更新此值。0 = 未填写,将跳过内存预算检查。")]
|
||
[Min(0)]
|
||
public int EstimatedMemoryKB;
|
||
|
||
private void OnValidate()
|
||
{
|
||
// 保证 GridSize 每轴最小为 1,防止零尺寸房间导致碰撞和渲染异常
|
||
GridSize = new Vector2Int(Mathf.Max(1, GridSize.x), Mathf.Max(1, GridSize.y));
|
||
|
||
// R11-N11 自动修剪 RoomId 首尾空格,避免 " Room_A " 与 "Room_A" 被视为不同键
|
||
if (!string.IsNullOrEmpty(RoomId) && RoomId != RoomId.Trim())
|
||
RoomId = RoomId.Trim();
|
||
|
||
// R12-N9 将旧 bool 字段迁移到 RoomFlags(RoomFlags 为 None 且旧字段有值时执行一次)
|
||
if (RoomFlags == RoomType.None)
|
||
{
|
||
if (IsBossRoom) RoomFlags |= RoomType.BossRoom;
|
||
if (IsSavePoint) RoomFlags |= RoomType.SavePoint;
|
||
if (IsShop) RoomFlags |= RoomType.Shop;
|
||
}
|
||
|
||
#if UNITY_EDITOR
|
||
// R11-N2 先 -= 再 +=,保证同一 delayCall 序列中最多执行一次,
|
||
// 防止 Inspector 快速拖动滑条时重复追加 N×FindAssets 导致卡顿
|
||
UnityEditor.EditorApplication.delayCall -= NotifyOwningDatabases;
|
||
UnityEditor.EditorApplication.delayCall += NotifyOwningDatabases;
|
||
#endif
|
||
}
|
||
|
||
#if UNITY_EDITOR
|
||
private void NotifyOwningDatabases()
|
||
{
|
||
if (this == null) return;
|
||
var guids = UnityEditor.AssetDatabase.FindAssets("t:MapDatabaseSO");
|
||
foreach (var guid in guids)
|
||
{
|
||
var path = UnityEditor.AssetDatabase.GUIDToAssetPath(guid);
|
||
var db = UnityEditor.AssetDatabase.LoadAssetAtPath<MapDatabaseSO>(path);
|
||
if (db?.AllRooms == null) continue;
|
||
if (System.Array.IndexOf(db.AllRooms, this) < 0) continue;
|
||
db.InvalidateIndex();
|
||
|
||
// Play Mode 下同时广播事件,让 UI 立即重建
|
||
if (UnityEngine.Application.isPlaying)
|
||
BaseGames.Core.ServiceLocator.GetOrDefault<IMapService>()?.NotifyDatabaseChanged();
|
||
}
|
||
}
|
||
#endif
|
||
}
|
||
|
||
[Serializable]
|
||
public struct RoomExitData
|
||
{
|
||
public string TargetRoomId; // 连接的目标房间 ID
|
||
public Vector2Int ExitGridPos; // 出口在格子地图上的位置
|
||
public ExitDirection Direction; // 出口方向
|
||
|
||
[Tooltip("此出口触发的过渡类型。\n" +
|
||
"Seamless:无缝切换(同区域相邻房间首选);\n" +
|
||
"AtmosphericFade:短暂淡出 + 区域名提示(跨大区域边界首选)。")]
|
||
public TransitionType PreferredTransitionType;
|
||
|
||
[Tooltip("是否已手动配置出口格子坐标。\n" +
|
||
"未勾选时,连线回退到房间中心,避免 (0,0) 与合法原点坐标产生歧义。")]
|
||
public bool HasCustomExitPos; // R12-N5 替代 ExitGridPos != Vector2Int.zero 哨兵用法
|
||
}
|
||
|
||
public enum ExitDirection { Up, Down, Left, Right }
|
||
|
||
// ─── 全局地图数据库 ──────────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 全局地图数据库 SO(编辑器配置一次;架构 15_MapShopModule §1.1)。
|
||
/// 资产路径: Assets/_Game/Data/Map/MapDatabase.asset
|
||
/// </summary>
|
||
[CreateAssetMenu(menuName = "BaseGames/World/Map/MapDatabase")]
|
||
public class MapDatabaseSO : ScriptableObject
|
||
{
|
||
[SerializeField, FormerlySerializedAs("AllRooms")]
|
||
private MapRoomDataSO[] _allRooms;
|
||
|
||
[SerializeField, Tooltip("勾选后,AssetPostprocessor 自动注册的新房间会优先加入此 Database;多个勾选时取 GUID 排序首个。")]
|
||
private bool _isDefault;
|
||
|
||
/// <summary>所属全部房间(只读视图)。编辑器写入请通过 <see cref="EditorSetRooms"/>。</summary>
|
||
public MapRoomDataSO[] AllRooms => _allRooms;
|
||
|
||
/// <summary>是否被标记为默认 Database。<see cref="MapRoomAutoRegister"/> 据此决定新建房间归属。</summary>
|
||
public bool IsDefault => _isDefault;
|
||
|
||
private Dictionary<string, MapRoomDataSO> _index;
|
||
private Dictionary<Vector2Int, string> _cellToRoom; // 格子坐标 → 房间 ID 空间索引(共享给 MinimapHUD/MapPlayerTracker,避免重复构建)
|
||
|
||
/// <summary>运行时快速查找(首次调用时建立索引)。</summary>
|
||
public MapRoomDataSO GetRoom(string roomId)
|
||
{
|
||
if (_index == null)
|
||
{
|
||
if (AllRooms == null) return null;
|
||
// R29-N2 使用 TryAdd(首条胜出),防止重复 RoomId 触发 ArgumentException;
|
||
// 编辑器侧 ValidateAll 负责提示策划修复数据,运行时继续工作不崩溃。
|
||
_index = new Dictionary<string, MapRoomDataSO>();
|
||
foreach (var r in AllRooms)
|
||
if (r != null && !string.IsNullOrEmpty(r.RoomId))
|
||
_index.TryAdd(r.RoomId, r);
|
||
}
|
||
_index.TryGetValue(roomId, out var r);
|
||
return r;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 在指定格子坐标处查询所属房间 ID(O(1) 哈希查找)。
|
||
/// 首次调用时惰性构建空间索引,由所有消费方(小地图/玩家追踪)共享,避免重复构建。
|
||
/// </summary>
|
||
public string GetRoomIdAtCell(Vector2Int cell)
|
||
{
|
||
EnsureSpatialIndex();
|
||
return _cellToRoom != null && _cellToRoom.TryGetValue(cell, out var id) ? id : null;
|
||
}
|
||
|
||
private void EnsureSpatialIndex()
|
||
{
|
||
if (_cellToRoom != null) return;
|
||
_cellToRoom = new Dictionary<Vector2Int, string>();
|
||
if (AllRooms == null) return;
|
||
foreach (var room in AllRooms)
|
||
{
|
||
if (room == null) continue;
|
||
for (int x = 0; x < room.GridSize.x; x++)
|
||
for (int y = 0; y < room.GridSize.y; y++)
|
||
_cellToRoom[new Vector2Int(room.GridPosition.x + x, room.GridPosition.y + y)] = room.RoomId;
|
||
}
|
||
}
|
||
|
||
/// <summary>数据库变更时(编辑器热改 / 运行时热更)强制让索引下次访问时重建。</summary>
|
||
public void InvalidateIndex()
|
||
{
|
||
_index = null;
|
||
_cellToRoom = null;
|
||
}
|
||
|
||
#if UNITY_EDITOR
|
||
/// <summary>
|
||
/// 编辑器专用:写入 <see cref="AllRooms"/> 数组并强制失效空间索引。
|
||
/// 替代直接赋值 public 字段,确保 <see cref="MapRoomAutoRegister"/> / 测试代码不绕过封装。
|
||
/// </summary>
|
||
public void EditorSetRooms(MapRoomDataSO[] rooms)
|
||
{
|
||
_allRooms = rooms;
|
||
InvalidateIndex();
|
||
}
|
||
#endif
|
||
|
||
private void OnDisable()
|
||
{
|
||
// SO 卸载时清理缓存
|
||
_index = null;
|
||
_cellToRoom = null;
|
||
}
|
||
|
||
private void OnValidate()
|
||
{
|
||
// 编辑器中修改 AllRooms 后强制重建索引
|
||
_index = null;
|
||
_cellToRoom = null;
|
||
#if UNITY_EDITOR
|
||
if (UnityEngine.Application.isPlaying)
|
||
BaseGames.Core.ServiceLocator.GetOrDefault<IMapService>()?.NotifyDatabaseChanged();
|
||
#endif
|
||
}
|
||
|
||
// ── 配置验证 ──────────────────────────────────────────────────────────
|
||
#if UNITY_EDITOR
|
||
/// <summary>
|
||
/// 检查数据库中的常见配置错误(RoomId 重复、格子重叠、出口悬空)。
|
||
/// 编辑器侧调用;运行时不应调用(有 O(N²) 开销)。
|
||
/// 返回错误描述列表;空列表表示无错误。
|
||
/// </summary>
|
||
public List<string> ValidateAll()
|
||
{
|
||
var errors = new List<string>();
|
||
if (AllRooms == null) return errors;
|
||
|
||
// ① null / 空 RoomId
|
||
for (int i = 0; i < AllRooms.Length; i++)
|
||
{
|
||
if (AllRooms[i] == null) { errors.Add($"AllRooms[{i}] 为 null"); continue; }
|
||
if (string.IsNullOrEmpty(AllRooms[i].RoomId))
|
||
errors.Add($"AllRooms[{i}]({AllRooms[i].name})RoomId 为空");
|
||
else
|
||
{
|
||
// R11-N11 首尾空格检查(OnValidate 已自动 Trim,此处兜底提示未经 OnValidate 的旧资产)
|
||
if (AllRooms[i].RoomId != AllRooms[i].RoomId.Trim())
|
||
errors.Add($"'{AllRooms[i].name}' RoomId 含首尾空格,请在 Inspector 中保存触发自动修剪");
|
||
// 特殊字符检查:/ \ | 等可能影响路径/键处理的字符
|
||
if (AllRooms[i].RoomId.IndexOfAny(new[]{ '/', '\\', '|', '<', '>', '*', '?' }) >= 0)
|
||
errors.Add($"'{AllRooms[i].RoomId}' 含非法字符(/ \\ | < > * ?),可能影响场景名匹配和存档键");
|
||
}
|
||
}
|
||
|
||
// ② RoomId 重复
|
||
var seenIds = new Dictionary<string, string>();
|
||
foreach (var room in AllRooms)
|
||
{
|
||
if (room == null || string.IsNullOrEmpty(room.RoomId)) continue;
|
||
if (seenIds.TryGetValue(room.RoomId, out var first))
|
||
errors.Add($"RoomId '{room.RoomId}' 重复({first} 与 {room.name})");
|
||
else
|
||
seenIds[room.RoomId] = room.name;
|
||
}
|
||
|
||
// ③ 格子重叠
|
||
var cellOwner = new Dictionary<Vector2Int, string>();
|
||
foreach (var room in 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);
|
||
if (cellOwner.TryGetValue(cell, out var other))
|
||
errors.Add($"'{room.RoomId}' 与 '{other}' 在格子 {cell} 重叠");
|
||
else
|
||
cellOwner[cell] = room.RoomId;
|
||
}
|
||
}
|
||
|
||
// ④ 出口目标不存在(单向验证)
|
||
var validIds = new HashSet<string>(seenIds.Keys);
|
||
foreach (var room in AllRooms)
|
||
{
|
||
if (room?.Exits == null) continue;
|
||
foreach (var exit in room.Exits)
|
||
if (!string.IsNullOrEmpty(exit.TargetRoomId) && !validIds.Contains(exit.TargetRoomId))
|
||
errors.Add($"'{room.RoomId}' 出口指向不存在的房间 '{exit.TargetRoomId}'");
|
||
}
|
||
|
||
return errors;
|
||
}
|
||
#endif
|
||
}
|
||
}
|