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.
This commit is contained in:
2026-05-25 23:15:12 +08:00
parent e2bc324905
commit f74d7f1877
53 changed files with 6825 additions and 270 deletions

View File

@@ -39,6 +39,13 @@ namespace BaseGames.Editor.Map
{
_database = (MapDatabaseSO)target;
_errorRowStyle = null; // 编辑器皮肤切换时(亮/暗模式)需重建
// 自动验证Inspector 打开时即填充错误集,无需手动点击"重新验证"按钮
// 防御性 null 检查CustomEditor.OnEnable 在反序列化失败的资产上仍会调用
if (_database != null)
{
_lastErrors = _database.ValidateAll();
RebuildErrorRoomCache();
}
}
/// <summary>错误行 GUIStyle 惰性初始化,基于当前编辑器皮肤构建,避免 OnInspectorGUI 每帧分配。</summary>
@@ -50,6 +57,21 @@ namespace BaseGames.Editor.Map
return _errorRowStyle;
}
/// <summary>
/// 基于 _lastErrors 重建 _cachedErrorRoomIds 集合。
/// 使用引号匹配防止 "Room_A1" 误匹配 "Room_A10"。
/// 由 OnEnable 自动验证与"重新验证"按钮共享调用。
/// </summary>
private void RebuildErrorRoomCache()
{
_cachedErrorRoomIds.Clear();
if (_lastErrors == null || _lastErrors.Count == 0 || _database.AllRooms == null) return;
foreach (var err in _lastErrors)
foreach (var r in _database.AllRooms)
if (r != null && err.Contains($"'{r.RoomId}'"))
_cachedErrorRoomIds.Add(r.RoomId);
}
public override void OnInspectorGUI()
{
serializedObject.Update();
@@ -77,14 +99,7 @@ namespace BaseGames.Editor.Map
if (GUILayout.Button(LabelValidate, GUILayout.Height(28)))
{
_lastErrors = _database.ValidateAll();
// 构建错误 RoomId 集合(使用引号匹配,防止 "Room_A1" 误匹配 "Room_A10"
_cachedErrorRoomIds.Clear();
if (_lastErrors.Count > 0 && _database.AllRooms != null)
foreach (var err in _lastErrors)
foreach (var r in _database.AllRooms)
if (r != null && err.Contains($"'{r.RoomId}'"))
_cachedErrorRoomIds.Add(r.RoomId);
RebuildErrorRoomCache();
if (_lastErrors.Count == 0)
Debug.Log("[MapDatabase] 验证通过,未发现问题 ✓");

View File

@@ -32,8 +32,20 @@ namespace BaseGames.Editor.Map
private List<string> _validationErrors;
private HashSet<string> _errorRoomIds;
/// <summary>缓存的错误房间集合,由验证按钮点击时重建(防止 OnInspectorGUI 高频重建导致 GC。</summary>
private readonly HashSet<string> _cachedErrorRoomIds = new();
// R9-N2 房间拖拽编辑状态
private MapRoomDataSO _draggedRoom;
private Vector2Int _dragOriginGridPos;
private Vector2 _dragMouseStart;
private bool _dragHasConflict; // R10-N5 当前拖拽位置与其它房间重叠时为 true
// R9-N4 搜索/图例
private string _searchText = string.Empty;
private bool _showLegend = true;
/// <summary>N4: DrawExitLines 去重集,缓存为字段避免每次 OnGUI 分配。</summary>
private readonly HashSet<(string, string)> _drawnExitPairs = new();
/// <summary>N4: DrawExitLines 连接线颜色,缓存为字段避免每次 OnGUI 分配。</summary>
private static readonly Color ExitLineColor = new Color(1f, 1f, 0.5f, 0.35f);
private readonly Dictionary<string, Color> _regionColors = new();
private int _paletteIndex;
@@ -42,6 +54,7 @@ namespace BaseGames.Editor.Map
private GUIStyle _roomLabelStyle;
private GUIStyle _badgeBossStyle;
private GUIStyle _badgeNormalStyle;
private GUIStyle _noResultStyle; // R18-N2 缓存"搜索无结果"提示样式zoom 无关,初始化一次)
private float _cachedZoomForStyle = -1f; // 用于检测缩放变化时重建 Style
// 区域配色方案(与 MapExploration 标注颜色视觉呼应)
@@ -78,6 +91,14 @@ namespace BaseGames.Editor.Map
Repaint();
}
/// <summary>R11-N7 项目资产变更(导入/删除/重命名)后清除验证缓存并触发重绘。</summary>
private void OnProjectChange()
{
_validationErrors = null;
_errorRoomIds = null;
Repaint();
}
private void OnGUI()
{
DrawToolbar();
@@ -97,6 +118,14 @@ namespace BaseGames.Editor.Map
EditorGUILayout.HelpBox(msg, MessageType.Warning);
}
// R10-N5 拖拽冲突提示
if (_draggedRoom != null && _dragHasConflict)
{
EditorGUILayout.HelpBox(
$"⚠ 房间 '{_draggedRoom.RoomId}' 当前位置与其它房间格子重叠,请调整。",
MessageType.Error);
}
// 地图绘制区域
Rect mapRect = GUILayoutUtility.GetRect(
GUIContent.none, GUIStyle.none,
@@ -137,6 +166,25 @@ namespace BaseGames.Editor.Map
Repaint();
}
// R9-N4 / R24-N3 搜索框:输入 RoomId / RegionId 子串或 RoomType 枚举名高亮匹配房间;✕ 按钮一键清空
GUILayout.Label("搜索", GUILayout.Width(32));
var newSearch = EditorGUILayout.TextField(_searchText, EditorStyles.toolbarTextField, GUILayout.Width(120));
if (newSearch != _searchText)
{
_searchText = newSearch;
Repaint();
}
GUI.enabled = !string.IsNullOrEmpty(_searchText);
if (GUILayout.Button("✕", EditorStyles.toolbarButton, GUILayout.Width(22)))
{
_searchText = string.Empty;
GUI.FocusControl(null);
Repaint();
}
GUI.enabled = true;
_showLegend = GUILayout.Toggle(_showLegend, "图例", EditorStyles.toolbarButton, GUILayout.Width(48));
EditorGUILayout.LabelField($"缩放 {_zoom:F0}px/格", GUILayout.Width(80));
EditorGUILayout.EndHorizontal();
}
@@ -163,6 +211,23 @@ namespace BaseGames.Editor.Map
e.Use();
break;
// R9-N2 左键按下(不带 Alt若命中房间则进入"房间拖拽"模式
case EventType.MouseDown when e.button == 0 && !e.alt:
{
var hit = HitTestRoom(e.mousePosition - mapRect.position, mapRect);
if (hit != null)
{
_draggedRoom = hit;
_dragOriginGridPos = hit.GridPosition;
_dragMouseStart = e.mousePosition;
_selectedRoom = hit;
Selection.activeObject = hit;
EditorGUIUtility.PingObject(hit);
e.Use();
}
break;
}
case EventType.MouseDrag when _isDragging:
_panOffset += e.mousePosition - _dragStart;
_dragStart = e.mousePosition;
@@ -170,18 +235,75 @@ namespace BaseGames.Editor.Map
Repaint();
break;
// R9-N2 房间拖拽:将像素偏移换算为格子偏移,实时更新 GridPosition
case EventType.MouseDrag when _draggedRoom != null:
{
Vector2 delta = e.mousePosition - _dragMouseStart;
int gx = Mathf.RoundToInt(delta.x / _zoom);
int gy = Mathf.RoundToInt(-delta.y / _zoom); // 屏幕 Y 向下→格子 Y 向上
var newPos = new Vector2Int(_dragOriginGridPos.x + gx, _dragOriginGridPos.y + gy);
if (newPos != _draggedRoom.GridPosition)
{
Undo.RecordObject(_draggedRoom, "Move Room");
_draggedRoom.GridPosition = newPos;
EditorUtility.SetDirty(_draggedRoom);
// R10-N5 重叠检测:候选位置覆盖的任意格子被其它房间占用即标冲突
_dragHasConflict = HasOverlapAt(_draggedRoom, newPos);
Repaint();
}
e.Use();
break;
}
case EventType.MouseUp when _isDragging:
_isDragging = false;
e.Use();
break;
// 左键(非 Alt点击选中房间
case EventType.MouseUp when e.button == 0 && !e.alt:
TrySelectRoom(e.mousePosition - mapRect.position, mapRect);
// R9-N2 房间拖拽结束
case EventType.MouseUp when _draggedRoom != null && e.button == 0:
_draggedRoom = null;
_dragHasConflict = false;
e.Use();
Repaint();
break;
}
}
/// <summary>命中测试:返回鼠标位置覆盖的房间(用于选中/拖拽起始判定)。</summary>
private MapRoomDataSO HitTestRoom(Vector2 clipPos, Rect mapRect)
{
if (_database?.AllRooms == null) return null;
Vector2 origin = mapRect.size * 0.5f + _panOffset;
foreach (var room in _database.AllRooms)
{
if (room == null) continue;
if (RoomToClipRect(room, origin).Contains(clipPos))
return room;
}
return null;
}
/// <summary>
/// R10-N5 检测候选房间放置在 <paramref name="newPos"/> 时是否与其它房间格子重叠。
/// 排除自身;用于拖拽时实时给策划红色反馈。
/// </summary>
private bool HasOverlapAt(MapRoomDataSO room, Vector2Int newPos)
{
if (_database?.AllRooms == null || room == null) return false;
int minX = newPos.x, maxX = newPos.x + room.GridSize.x;
int minY = newPos.y, maxY = newPos.y + room.GridSize.y;
foreach (var other in _database.AllRooms)
{
if (other == null || other == room) continue;
int oMinX = other.GridPosition.x, oMaxX = oMinX + other.GridSize.x;
int oMinY = other.GridPosition.y, oMaxY = oMinY + other.GridSize.y;
bool overlap = minX < oMaxX && maxX > oMinX && minY < oMaxY && maxY > oMinY;
if (overlap) return true;
}
return false;
}
// ── 地图绘制 ──────────────────────────────────────────────────────────
private void DrawMapArea(Rect mapRect)
@@ -193,6 +315,9 @@ namespace BaseGames.Editor.Map
Vector2 origin = mapRect.size * 0.5f + _panOffset;
// N3 搜索有内容时追踪是否有任何匹配项,无结果时显示提示
bool anySearchMatch = false;
foreach (var room in _database.AllRooms)
{
if (room == null) continue;
@@ -201,10 +326,25 @@ namespace BaseGames.Editor.Map
// 填充
bool hasError = _errorRoomIds != null && _errorRoomIds.Contains(room.RoomId);
// R23-N2 搜索支持 RoomId / RegionId 子串 以及 RoomType 枚举名(如 "SavePoint"、"BossRoom"
bool matchesSearch = !string.IsNullOrEmpty(_searchText)
&& (room.RoomId?.IndexOf(_searchText, System.StringComparison.OrdinalIgnoreCase) >= 0
|| room.RegionId?.IndexOf(_searchText, System.StringComparison.OrdinalIgnoreCase) >= 0
|| MatchesRoomType(room.RoomFlags, _searchText));
if (matchesSearch) anySearchMatch = true;
// R10-N5 正在被拖拽且与其它房间重叠时优先显示红色警示
bool draggingConflict = room == _draggedRoom && _dragHasConflict;
Color regionColor = GetRegionColor(room.RegionId);
Color fillColor = hasError
? new Color(1f, 0.15f, 0.15f, 0.55f)
: new Color(regionColor.r, regionColor.g, regionColor.b, 0.28f);
Color fillColor = draggingConflict
? new Color(1f, 0.1f, 0.1f, 0.75f)
: hasError
? new Color(1f, 0.15f, 0.15f, 0.55f)
: matchesSearch
? new Color(1f, 0.95f, 0.2f, 0.55f)
// N2 搜索活跃时非匹配房间降低 alpha使匹配项更突出
: !string.IsNullOrEmpty(_searchText)
? new Color(regionColor.r, regionColor.g, regionColor.b, 0.08f)
: new Color(regionColor.r, regionColor.g, regionColor.b, 0.28f);
EditorGUI.DrawRect(cell, fillColor);
// 边框
@@ -226,7 +366,75 @@ namespace BaseGames.Editor.Map
if (_zoom >= 12f)
DrawExitLines(origin);
// R9-N11 Play Mode 玩家位置叠加:在当前房间上绘制醒目红点
if (Application.isPlaying)
DrawPlayModePlayerDot(origin);
// N3 搜索无结果时居中显示提示文本
if (!string.IsNullOrEmpty(_searchText) && !anySearchMatch)
{
GUI.Label(new Rect(0f, 0f, mapRect.width, mapRect.height),
$"未找到与 \"{_searchText}\" 匹配的房间", _noResultStyle);
}
GUI.EndClip();
// R9-N4 图例面板(独立绘制,不参与裁剪)
if (_showLegend)
DrawLegendPanel(mapRect);
}
private void DrawPlayModePlayerDot(Vector2 origin)
{
var provider = BaseGames.Core.ServiceLocator.GetOrDefault<IPlayerPositionProvider>();
if (provider == null) return;
var roomId = provider.CurrentRoomId;
if (string.IsNullOrEmpty(roomId)) return;
var room = _database.GetRoom(roomId);
if (room == null) return;
Rect cell = RoomToClipRect(room, origin);
Vector2 norm = provider.NormalizedPositionInRoom;
// norm.y=0 是房间底部;屏幕 Y 向下需翻转
float px = cell.x + norm.x * cell.width;
float py = cell.y + (1f - norm.y) * cell.height;
float r = Mathf.Max(4f, _zoom * 0.18f);
var prev = GUI.color;
GUI.color = new Color(1f, 0.2f, 0.2f, 0.95f);
GUI.DrawTexture(new Rect(px - r, py - r, r * 2f, r * 2f), EditorGUIUtility.whiteTexture);
GUI.color = Color.white;
DrawBorder(new Rect(px - r - 1f, py - r - 1f, r * 2f + 2f, r * 2f + 2f), Color.white, 1f);
GUI.color = prev;
// 持续刷新使 Play Mode 玩家位置可视化跟随移动
Repaint();
}
private void DrawLegendPanel(Rect mapRect)
{
const float W = 200f;
float h = 26f + _regionColors.Count * 18f + (Application.isPlaying ? 18f : 0f);
var panel = new Rect(mapRect.xMax - W - 8f, mapRect.y + 8f, W, h);
EditorGUI.DrawRect(panel, new Color(0.12f, 0.12f, 0.12f, 0.85f));
DrawBorder(panel, new Color(0.4f, 0.4f, 0.4f, 1f), 1f);
var labelStyle = EditorStyles.miniBoldLabel;
GUI.Label(new Rect(panel.x + 6f, panel.y + 4f, W - 12f, 16f), "Region 图例", labelStyle);
float y = panel.y + 22f;
foreach (var kv in _regionColors)
{
EditorGUI.DrawRect(new Rect(panel.x + 8f, y + 3f, 12f, 12f), kv.Value);
GUI.Label(new Rect(panel.x + 24f, y, W - 30f, 16f),
string.IsNullOrEmpty(kv.Key) ? "(无 Region)" : kv.Key, EditorStyles.miniLabel);
y += 18f;
}
if (Application.isPlaying)
{
EditorGUI.DrawRect(new Rect(panel.x + 8f, y + 4f, 10f, 10f), new Color(1f, 0.2f, 0.2f));
GUI.Label(new Rect(panel.x + 24f, y, W - 30f, 16f), "玩家位置Play Mode", EditorStyles.miniLabel);
}
}
/// <summary>
@@ -235,6 +443,17 @@ namespace BaseGames.Editor.Map
/// </summary>
private void EnsureLabelStyles()
{
// _noResultStyle 不依赖 zoom初始化一次即可
if (_noResultStyle == null)
{
_noResultStyle = new GUIStyle(EditorStyles.boldLabel)
{
alignment = TextAnchor.MiddleCenter,
normal = { textColor = new Color(1f, 0.8f, 0.2f, 0.8f) },
fontSize = 13,
};
}
if (Mathf.Approximately(_cachedZoomForStyle, _zoom) && _roomLabelStyle != null) return;
_cachedZoomForStyle = _zoom;
@@ -267,18 +486,27 @@ namespace BaseGames.Editor.Map
private void DrawRoomBadge(Rect cell, MapRoomDataSO room)
{
if (!room.IsBossRoom && !room.IsSavePoint && !room.IsShop) return;
// R12-N9 优先检查 RoomFlags新 [Flags] 枚举),回退兼容旧 bool 字段
// R15-N1 补充 TeleportStation与运行时 MapRoomDataSO.ChooseDisplayIcon 保持一致
bool isBoss = room.RoomFlags.HasFlag(RoomType.BossRoom) || room.IsBossRoom;
bool isSave = room.RoomFlags.HasFlag(RoomType.SavePoint) || room.IsSavePoint;
bool isShop = room.RoomFlags.HasFlag(RoomType.Shop) || room.IsShop;
bool isTeleport = room.RoomFlags.HasFlag(RoomType.TeleportStation);
if (!isBoss && !isSave && !isShop && !isTeleport) return;
if (cell.width < 8f) return;
string badge = room.IsBossRoom ? "★" :
room.IsSavePoint ? "" : "¥";
GUI.Label(cell, badge, room.IsBossRoom ? _badgeBossStyle : _badgeNormalStyle);
// 优先级与运行时 MapRoomDataSO.ChooseDisplayIcon 对齐Save > Boss > Shop > Teleport
string badge = isSave ? "♦" : isBoss ? "★" : isShop ? "¥" : "";
// 徽章样式与实际显示的徽章类型对齐(不随 isBoss 变化,而是随 badge 内容)
GUI.Label(cell, badge, badge == "★" ? _badgeBossStyle : _badgeNormalStyle);
}
private void DrawExitLines(Vector2 origin)
{
if (_database?.AllRooms == null) return;
var lineColor = new Color(1f, 1f, 0.5f, 0.35f);
// R12-N2 去重A→B 和 B→A 各画一次,用规范化有序键防止重复绘制
// N4: 复用字段级 HashSet避免每次 OnGUI 分配新对象
_drawnExitPairs.Clear();
foreach (var room in _database.AllRooms)
{
@@ -288,10 +516,34 @@ namespace BaseGames.Editor.Map
var target = _database.GetRoom(exit.TargetRoomId);
if (target == null) continue;
Vector2 from = GridCenterToClip(room.GridPosition + room.GridSize / 2, origin);
Vector2 to = GridCenterToClip(target.GridPosition + target.GridSize / 2, origin);
// R12-N2 规范化键:按 Ordinal 比较,较小 ID 在前,避免 A-B / B-A 重复画线
var keyA = string.CompareOrdinal(room.RoomId, exit.TargetRoomId) <= 0
? (room.RoomId, exit.TargetRoomId)
: (exit.TargetRoomId, room.RoomId);
if (!_drawnExitPairs.Add(keyA)) continue;
DrawLine(from, to, lineColor, 1.5f);
// R12-N5 使用 HasCustomExitPos 替代 != Vector2Int.zero 哨兵,避免合法原点坐标歧义
Vector2Int fromGrid = exit.HasCustomExitPos
? exit.ExitGridPos
: room.GridPosition + room.GridSize / 2;
// 找目标房间中与本出口对应的反向出口(方向相反且 TargetRoomId == room.RoomId
Vector2Int toGrid = target.GridPosition + target.GridSize / 2; // fallback
if (target.Exits != null)
{
foreach (var rev in target.Exits)
{
if (rev.TargetRoomId == room.RoomId && rev.HasCustomExitPos)
{
toGrid = rev.ExitGridPos;
break;
}
}
}
Vector2 from = GridCenterToClip(fromGrid, origin);
Vector2 to = GridCenterToClip(toGrid, origin);
DrawLine(from, to, ExitLineColor, 1.5f);
}
}
}
@@ -311,30 +563,8 @@ namespace BaseGames.Editor.Map
}
// ── 房间选择 ──────────────────────────────────────────────────────────
private void TrySelectRoom(Vector2 clipPos, Rect mapRect)
{
if (_database?.AllRooms == null) return;
// 使用与 DrawMapArea 完全相同的 origin 公式,确保命中测试一致
Vector2 origin = mapRect.size * 0.5f + _panOffset;
foreach (var room in _database.AllRooms)
{
if (room == null) continue;
Rect cell = RoomToClipRect(room, origin);
if (cell.Contains(clipPos))
{
_selectedRoom = room;
Selection.activeObject = room;
EditorGUIUtility.PingObject(room);
Repaint();
return;
}
}
_selectedRoom = null;
Repaint();
}
// R9-N2 后由 HandleInput 中的 MouseDown 直接调用 HitTestRoom 完成选中+起拖,
// 旧的 TrySelectRoom仅在 MouseUp 时选中)已合并到 HandleInput。
// ── 工具方法 ──────────────────────────────────────────────────────────
@@ -361,6 +591,23 @@ namespace BaseGames.Editor.Map
return c;
}
/// <summary>
/// R23-N2 检查房间的 RoomFlags 是否包含与 searchText 大小写不敏感匹配的 RoomType 枚举成员。
/// 允许策划通过搜索 "SavePoint"、"BossRoom" 等关键字快速过滤特定类型的房间。
/// </summary>
private static bool MatchesRoomType(RoomType flags, string searchText)
{
if (flags == RoomType.None) return false;
foreach (RoomType value in System.Enum.GetValues(typeof(RoomType)))
{
if (value == RoomType.None) continue;
if (!flags.HasFlag(value)) continue;
if (value.ToString().IndexOf(searchText, System.StringComparison.OrdinalIgnoreCase) >= 0)
return true;
}
return false;
}
private void RunValidation()
{
_validationErrors = _database?.ValidateAll() ?? new List<string>();
@@ -399,11 +646,18 @@ namespace BaseGames.Editor.Map
var prevMatrix = GUI.matrix;
var prevColor = GUI.color;
GUI.color = color;
GUIUtility.RotateAroundPivot(angle, mid);
GUI.DrawTexture(new Rect(mid.x - len * 0.5f, mid.y - thickness * 0.5f, len, thickness),
EditorGUIUtility.whiteTexture);
GUI.matrix = prevMatrix;
GUI.color = prevColor;
// R11-N12 try/finally 保证 GUI.matrix/color 在 DrawTexture 抛出异常时仍能恢复
try
{
GUIUtility.RotateAroundPivot(angle, mid);
GUI.DrawTexture(new Rect(mid.x - len * 0.5f, mid.y - thickness * 0.5f, len, thickness),
EditorGUIUtility.whiteTexture);
}
finally
{
GUI.matrix = prevMatrix;
GUI.color = prevColor;
}
}
}
}

View File

@@ -0,0 +1,105 @@
#if UNITY_EDITOR
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using BaseGames.World.Map;
namespace BaseGames.Editor.Map
{
/// <summary>
/// 新建 MapRoomDataSO 时自动追加到默认 MapDatabaseSO 的 AllRooms 列表R9-N3
/// 解决问题:开发/策划新建房间后忘记手动拖入 Database导致运行时小地图缺失房间。
/// <para>
/// 触发条件:检测到导入的 MapRoomDataSO 资产路径中包含项目内任意 Database 引用即跳过;
/// 若不被任何 Database 引用,则追加到"默认"Database首个被找到的 MapDatabaseSO
/// 按 AssetDatabase.FindAssets GUID 排序保证可复现)。
/// </para>
/// 不会修改资产文件层级结构;仅修改 Database 的 AllRooms 数组并 SetDirty。
/// 可在 EditorPrefs 中通过 Key <see cref="DISABLE_KEY"/> 设为 true 来禁用。
/// </summary>
public class MapRoomAutoRegister : AssetPostprocessor
{
private const string DISABLE_KEY = "BaseGames.Map.AutoRegister.Disabled";
private static void OnPostprocessAllAssets(
string[] importedAssets,
string[] deletedAssets,
string[] movedAssets,
string[] movedFromAssetPaths)
{
if (EditorPrefs.GetBool(DISABLE_KEY, false)) return;
if (importedAssets == null || importedAssets.Length == 0) return;
// 仅处理新导入的 MapRoomDataSO含 .asset 后缀过滤,减少 LoadAssetAtPath 调用)
List<MapRoomDataSO> newRooms = null;
foreach (var path in importedAssets)
{
if (!path.EndsWith(".asset", System.StringComparison.OrdinalIgnoreCase)) continue;
var room = AssetDatabase.LoadAssetAtPath<MapRoomDataSO>(path);
if (room == null) continue;
(newRooms ??= new List<MapRoomDataSO>()).Add(room);
}
if (newRooms == null) return;
// 查找项目内所有 MapDatabaseSO按 GUID 排序确保"默认 Database"在多人协作下一致)
var dbGuids = AssetDatabase.FindAssets($"t:{nameof(MapDatabaseSO)}");
if (dbGuids.Length == 0) return;
var databases = dbGuids
.OrderBy(g => g, System.StringComparer.Ordinal)
.Select(g => AssetDatabase.LoadAssetAtPath<MapDatabaseSO>(AssetDatabase.GUIDToAssetPath(g)))
.Where(db => db != null)
.ToList();
if (databases.Count == 0) return;
// R10-N4 默认 Database 选择优先级:
// 1) 显式勾选 IsDefault 的首个GUID 排序);
// 2) 否则回退 GUID 排序首个。
var defaultDb = databases.FirstOrDefault(db => db.IsDefault) ?? databases[0];
bool dirty = false;
foreach (var room in newRooms)
{
// 已被任意 Database 引用则跳过(避免重复追加 + 跨 Database 误抢占)
if (databases.Any(db => db.AllRooms != null && System.Array.IndexOf(db.AllRooms, room) >= 0))
continue;
var arr = defaultDb.AllRooms ?? System.Array.Empty<MapRoomDataSO>();
var newArr = new MapRoomDataSO[arr.Length + 1];
System.Array.Copy(arr, newArr, arr.Length);
newArr[arr.Length] = room;
defaultDb.EditorSetRooms(newArr);
dirty = true;
Debug.Log($"[MapRoomAutoRegister] 已将新房间 '{room.RoomId}' 自动注册到 Database '{defaultDb.name}'。", defaultDb);
}
if (dirty)
{
EditorUtility.SetDirty(defaultDb);
// 不主动 SaveAssets避免在批量导入流程中拖慢 IDE下次工程保存自动落盘
}
// R12-N4 清理所有 Database 中的 null 条目(资产被删除后留下的空引用)
foreach (var db in databases)
{
if (db.AllRooms == null) continue;
if (!System.Array.Exists(db.AllRooms, r => r == null)) continue;
var cleaned = System.Array.FindAll(db.AllRooms, r => r != null);
int nullCount = db.AllRooms.Length - cleaned.Length;
db.EditorSetRooms(cleaned);
EditorUtility.SetDirty(db);
Debug.Log($"[MapRoomAutoRegister] 已清理 Database '{db.name}' 中的 {nullCount} 个 null 条目。", db);
}
}
[MenuItem("Tools/Map/Toggle Auto-Register New Rooms")]
private static void ToggleAutoRegister()
{
bool disabled = EditorPrefs.GetBool(DISABLE_KEY, false);
EditorPrefs.SetBool(DISABLE_KEY, !disabled);
Debug.Log($"[MapRoomAutoRegister] 自动注册已 {(!disabled ? "" : "")}。");
}
}
}
#endif

View File

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

View File

@@ -11,6 +11,7 @@ namespace BaseGames.Editor.Map
/// 拖动自动吸附到整格精度;左下/右上角可独立拖动(含反转保护);支持 Undo。
/// </summary>
[CustomEditor(typeof(MapRoomDataSO))]
[CanEditMultipleObjects]
public class MapRoomDataEditor : UnityEditor.Editor
{
private const float CELL_SIZE = 1f; // 每格在 Scene 中的世界单位尺寸
@@ -19,13 +20,23 @@ namespace BaseGames.Editor.Map
private static readonly Color OutlineColor = new Color(0.2f, 0.6f, 1f, 0.9f);
private static readonly Color HandleColor = new Color(1f, 0.85f, 0.2f, 1f);
private static readonly GUIStyle LabelStyle = new GUIStyle
// 静态 GUIStyle 在域重载/字段初始化器顺序下可能产生 NREnormal.background=null 访问)。
// 改用懒加载 + 首次访问时构建,避免 EditorStyles 未初始化导致的访问问题。
private static GUIStyle s_labelStyle;
private static GUIStyle s_cornerLabelStyle;
private static GUIStyle LabelStyle => s_labelStyle ??= new GUIStyle(EditorStyles.boldLabel)
{
alignment = TextAnchor.MiddleCenter,
fontStyle = FontStyle.Bold,
normal = { textColor = Color.white },
};
private static GUIStyle CornerLabelStyle => s_cornerLabelStyle ??= new GUIStyle(EditorStyles.miniLabel)
{
alignment = TextAnchor.MiddleLeft,
normal = { textColor = new Color(1f, 0.85f, 0.2f, 1f) },
};
private MapRoomDataSO _target;
private void OnEnable() => _target = (MapRoomDataSO)target;
@@ -101,6 +112,8 @@ namespace BaseGames.Editor.Map
Color prev = Handles.color;
Handles.color = HandleColor;
var result = Handles.FreeMoveHandle(pos, sz, Vector3.zero, Handles.DotHandleCap);
// 角点标签:标识 BL / TR 角,便于多房间布局编辑时区分;偏移避免遮挡 handle
Handles.Label(pos + new Vector3(sz * 1.5f, sz * 0.5f, 0f), label, CornerLabelStyle);
Handles.color = prev;
return SnapToGrid(result);
}