Files
Cielonos/Assets/Scripts/MainGame/GameRun/Map/MapPreviewer.cs
SoulliesOfficial 47125f95f4 地图初步
2026-04-30 07:06:38 -04:00

397 lines
15 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
}