397 lines
15 KiB
C#
397 lines
15 KiB
C#
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
|
||
{
|
||
/// <summary>
|
||
/// 地图预览器,在 Inspector 中可视化生成并展示全联通网格地图。
|
||
/// </summary>
|
||
[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<Vector2Int, Vector2> screenPositions = ComputeScreenPositions(canvasRect);
|
||
|
||
// 绘制连线(先画)
|
||
DrawEdges(screenPositions);
|
||
|
||
// 绘制节点
|
||
DrawNodes(screenPositions);
|
||
|
||
// 图例
|
||
DrawLegend(canvasRect);
|
||
|
||
// 统计信息
|
||
DrawStats(canvasRect);
|
||
|
||
GUILayout.Space(4f);
|
||
}
|
||
|
||
// ----------------------------------------------------------------
|
||
// 坐标映射
|
||
// ----------------------------------------------------------------
|
||
|
||
private Dictionary<Vector2Int, Vector2> ComputeScreenPositions(Rect canvas)
|
||
{
|
||
Dictionary<Vector2Int, Vector2> result = new Dictionary<Vector2Int, Vector2>();
|
||
|
||
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<Vector2Int, Vector2> 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<Vector2Int, RunMapNode> 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<Vector2Int, Vector2> positions)
|
||
{
|
||
if (Event.current.type != EventType.Repaint) return;
|
||
|
||
foreach (KeyValuePair<Vector2Int, RunMapNode> 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<MapNodeType, int> counts = new Dictionary<MapNodeType, int>();
|
||
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
|
||
}
|
||
}
|