using System.Collections.Generic; using System.Linq; using Cielonos.MainGame.Map; using Sirenix.OdinInspector; using UnityEngine; #if UNITY_EDITOR using Sirenix.Utilities.Editor; using UnityEditor; #endif namespace Cielonos.MainGame.Map { /// /// 地图预览器,在 Inspector 中可视化生成并展示全联通网格地图。 /// [CreateAssetMenu(fileName = "MapPreviewer", menuName = "Cielonos/Map/MapPreviewer")] public class MapPreviewer : SerializedScriptableObject { // ---------------------------------------------------------------- // 配置 // ---------------------------------------------------------------- [Title("配置")] [Required("需要指定 MapGenerationConfig")] public MapGenerationConfig config; [Title("预览外观")] [Range(300f, 1000f)] public float previewSize = 600f; [MinValue(6f), MaxValue(24f)] public float nodeRadius = 12f; [Range(1f, 6f)] public float edgeThickness = 2.5f; // ---------------------------------------------------------------- // 节点颜色映射 // ---------------------------------------------------------------- [Title("节点颜色")] public Color colorStart = new Color(0.4f, 0.9f, 0.4f); public Color colorNormalCombat = new Color(0.85f, 0.35f, 0.35f); public Color colorEliteCombat = new Color(0.9f, 0.55f, 0.15f); public Color colorBossCombat = new Color(0.7f, 0.15f, 0.85f); public Color colorMechanicalTable = new Color(0.3f, 0.7f, 1.0f); public Color colorLogisticsCenter = new Color(1.0f, 0.85f, 0.2f); public Color colorMedicalStation = new Color(0.3f, 0.85f, 0.6f); #if UNITY_EDITOR // ---------------------------------------------------------------- // 运行时缓存(仅 Editor) // ---------------------------------------------------------------- [HideInInspector] public RunMapData previewData; // ---------------------------------------------------------------- // 按钮 // ---------------------------------------------------------------- [Title("操作")] [Button("生成随机地图", ButtonSizes.Large), GUIColor(0.4f, 0.85f, 0.5f)] public void GeneratePreview() { if (config == null) { Debug.LogWarning("[MapPreviewer] 请先指定 MapGenerationConfig。"); return; } previewData = MapGenerator.Generate(config); EditorUtility.SetDirty(this); } [Button("清空预览", ButtonSizes.Medium), GUIColor(0.85f, 0.4f, 0.4f)] public void ClearPreview() { previewData = null; EditorUtility.SetDirty(this); } // ---------------------------------------------------------------- // Inspector 绘制 // ---------------------------------------------------------------- [OnInspectorGUI] private void DrawMapCanvas() { if (previewData == null || previewData.nodes == null || previewData.nodes.Count == 0) { SirenixEditorGUI.InfoMessageBox("尚未生成地图,请点击「生成随机地图」按钮。"); return; } GUILayout.Space(8f); SirenixEditorGUI.Title("地图预览", $"共 {previewData.totalNodes} 个房间", TextAlignment.Center, horizontalLine: true); GUILayout.Space(4f); // 预留正方形画布 Rect canvasRect = GUILayoutUtility.GetRect( GUIContent.none, GUIStyle.none, GUILayout.ExpandWidth(true), GUILayout.Height(previewSize) ); // 深色背景 SirenixEditorGUI.DrawSolidRect(canvasRect, new Color(0.12f, 0.12f, 0.15f, 1f)); // 计算屏幕坐标 Dictionary screenPositions = ComputeScreenPositions(canvasRect); // 绘制连线(先画) DrawEdges(screenPositions); // 绘制节点 DrawNodes(screenPositions); // 图例 DrawLegend(canvasRect); // 统计信息 DrawStats(canvasRect); GUILayout.Space(4f); } // ---------------------------------------------------------------- // 坐标映射 // ---------------------------------------------------------------- private Dictionary ComputeScreenPositions(Rect canvas) { Dictionary result = new Dictionary(); if (previewData.nodes.Count == 0) return result; // 找出网格坐标的边界 int minX = int.MaxValue, maxX = int.MinValue; int minY = int.MaxValue, maxY = int.MinValue; foreach (Vector2Int pos in previewData.nodes.Keys) { if (pos.x < minX) minX = pos.x; if (pos.x > maxX) maxX = pos.x; if (pos.y < minY) minY = pos.y; if (pos.y > maxY) maxY = pos.y; } int gridWidth = Mathf.Max(maxX - minX, 1); int gridHeight = Mathf.Max(maxY - minY, 1); const float paddingRatio = 0.08f; float padX = canvas.width * paddingRatio; float padY = canvas.height * paddingRatio; float cellW = (canvas.width - padX * 2f) / gridWidth; float cellH = (canvas.height - padY * 2f) / gridHeight; float cellSize = Mathf.Min(cellW, cellH); // 居中偏移 float totalW = gridWidth * cellSize; float totalH = gridHeight * cellSize; float offsetX = canvas.x + (canvas.width - totalW) * 0.5f; float offsetY = canvas.y + (canvas.height - totalH) * 0.5f; foreach (Vector2Int pos in previewData.nodes.Keys) { float sx = offsetX + (pos.x - minX) * cellSize; // Y 轴翻转:网格 Y 越大 → 画布 Y 越小(向上) float sy = offsetY + (maxY - pos.y) * cellSize; result[pos] = new Vector2(sx, sy); } return result; } // ---------------------------------------------------------------- // 连线绘制 // ---------------------------------------------------------------- private void DrawEdges(Dictionary positions) { if (Event.current.type != EventType.Repaint) return; Color edgeColor = new Color(0.45f, 0.45f, 0.55f, 0.7f); HashSet<(Vector2Int, Vector2Int)> drawnEdges = new HashSet<(Vector2Int, Vector2Int)>(); foreach (KeyValuePair kvp in previewData.nodes) { Vector2Int fromPos = kvp.Key; if (!positions.TryGetValue(fromPos, out Vector2 fromScreen)) continue; foreach (Vector2Int toPos in kvp.Value.connectedPositions) { // 避免双向连接绘制两次 var edgeKey = fromPos.x < toPos.x || (fromPos.x == toPos.x && fromPos.y < toPos.y) ? (fromPos, toPos) : (toPos, fromPos); if (drawnEdges.Contains(edgeKey)) continue; drawnEdges.Add(edgeKey); if (!positions.TryGetValue(toPos, out Vector2 toScreen)) continue; DrawThickLine(fromScreen, toScreen, edgeColor, edgeThickness); } } } private static void DrawThickLine(Vector2 from, Vector2 to, Color color, float thickness) { Handles.color = color; Vector2 dir = (to - from).normalized; Vector2 perp = new Vector2(-dir.y, dir.x); int steps = Mathf.Max(1, Mathf.RoundToInt(thickness)); float halfT = (steps - 1) * 0.5f; for (int i = 0; i < steps; i++) { float offset = (i - halfT) * 0.8f; Vector3 f3 = new Vector3(from.x + perp.x * offset, from.y + perp.y * offset, 0f); Vector3 t3 = new Vector3(to.x + perp.x * offset, to.y + perp.y * offset, 0f); Handles.DrawLine(f3, t3); } } // ---------------------------------------------------------------- // 节点绘制 // ---------------------------------------------------------------- private void DrawNodes(Dictionary positions) { if (Event.current.type != EventType.Repaint) return; foreach (KeyValuePair kvp in previewData.nodes) { if (!positions.TryGetValue(kvp.Key, out Vector2 center)) continue; RunMapNode node = kvp.Value; Color nodeColor = GetNodeColor(node.nodeType); // 外圈(深色边框) float outerR = nodeRadius + 2f; Handles.color = new Color(0.05f, 0.05f, 0.05f, 0.9f); Handles.DrawSolidDisc(new Vector3(center.x, center.y, 0f), Vector3.forward, outerR); // 节点填充 Handles.color = nodeColor; Handles.DrawSolidDisc(new Vector3(center.x, center.y, 0f), Vector3.forward, nodeRadius); // 节点标签 DrawNodeLabel(node, center); } } private void DrawNodeLabel(RunMapNode node, Vector2 center) { string label = GetNodeShortLabel(node.nodeType); GUIStyle style = new GUIStyle(EditorStyles.miniLabel) { alignment = TextAnchor.MiddleCenter, fontStyle = FontStyle.Bold, fontSize = 9, normal = { textColor = Color.white } }; float labelW = 48f; float labelH = 16f; Rect labelRect = new Rect(center.x - labelW * 0.5f, center.y + nodeRadius + 2f, labelW, labelH); GUI.Label(labelRect, label, style); } // ---------------------------------------------------------------- // 图例 // ---------------------------------------------------------------- private void DrawLegend(Rect canvas) { if (Event.current.type != EventType.Repaint) return; var entries = new (MapNodeType type, string label)[] { (MapNodeType.Start, "起点"), (MapNodeType.NormalCombat, "普通战斗"), (MapNodeType.EliteCombat, "精英战斗"), (MapNodeType.BossCombat, "Boss"), (MapNodeType.MechanicalTable, "机械台"), (MapNodeType.LogisticsCenter, "物流中心"), (MapNodeType.MedicalStation, "医疗站"), }; const float dotSize = 10f; const float itemH = 16f; const float itemW = 90f; const float padLeft = 8f; const float padBottom = 8f; float totalH = entries.Length * itemH + 4f; float startY = canvas.yMax - padBottom - totalH; Rect bgRect = new Rect(canvas.x + padLeft - 4f, startY - 2f, itemW + 8f, totalH + 4f); SirenixEditorGUI.DrawSolidRect(bgRect, new Color(0f, 0f, 0f, 0.45f)); GUIStyle labelStyle = new GUIStyle(EditorStyles.miniLabel) { normal = { textColor = new Color(0.85f, 0.85f, 0.85f) } }; for (int i = 0; i < entries.Length; i++) { float y = startY + i * itemH; Handles.color = GetNodeColor(entries[i].type); Handles.DrawSolidDisc( new Vector3(canvas.x + padLeft + dotSize * 0.5f, y + itemH * 0.5f, 0f), Vector3.forward, dotSize * 0.5f); Rect textRect = new Rect(canvas.x + padLeft + dotSize + 4f, y, itemW - dotSize - 4f, itemH); GUI.Label(textRect, entries[i].label, labelStyle); } } // ---------------------------------------------------------------- // 统计信息 // ---------------------------------------------------------------- private void DrawStats(Rect canvas) { if (Event.current.type != EventType.Repaint) return; // 统计各类型数量 Dictionary counts = new Dictionary(); foreach (RunMapNode node in previewData.nodes.Values) { if (!counts.ContainsKey(node.nodeType)) counts[node.nodeType] = 0; counts[node.nodeType]++; } string stats = string.Join(" | ", counts .OrderBy(kvp => kvp.Key) .Select(kvp => $"{GetNodeShortLabel(kvp.Key)}: {kvp.Value}")); GUIStyle style = new GUIStyle(EditorStyles.miniLabel) { alignment = TextAnchor.MiddleRight, normal = { textColor = new Color(0.7f, 0.7f, 0.7f) } }; const float padRight = 8f; const float padTop = 6f; Rect statsRect = new Rect(canvas.x, canvas.y + padTop, canvas.width - padRight, 16f); GUI.Label(statsRect, stats, style); } // ---------------------------------------------------------------- // 工具方法 // ---------------------------------------------------------------- private Color GetNodeColor(MapNodeType type) { return type switch { MapNodeType.Start => colorStart, MapNodeType.NormalCombat => colorNormalCombat, MapNodeType.EliteCombat => colorEliteCombat, MapNodeType.BossCombat => colorBossCombat, MapNodeType.MechanicalTable => colorMechanicalTable, MapNodeType.LogisticsCenter => colorLogisticsCenter, MapNodeType.MedicalStation => colorMedicalStation, _ => Color.white, }; } private static string GetNodeShortLabel(MapNodeType type) { return type switch { MapNodeType.Start => "Start", MapNodeType.NormalCombat => "Combat", MapNodeType.EliteCombat => "Elite", MapNodeType.BossCombat => "BOSS", MapNodeType.MechanicalTable => "Chest", MapNodeType.LogisticsCenter => "Shop", MapNodeType.MedicalStation => "Med", _ => "?", }; } #endif } }