地图初步
This commit is contained in:
396
Assets/Scripts/MainGame/GameRun/Map/MapPreviewer.cs
Normal file
396
Assets/Scripts/MainGame/GameRun/Map/MapPreviewer.cs
Normal file
@@ -0,0 +1,396 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user