Files
gold_dolphin/unity/Assets/Editor/SpritePrefabGeneratorWindow.cs
T
2026-07-01 14:27:57 +08:00

452 lines
16 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 UnityEngine;
using UnityEditor;
using System.Collections.Generic;
/// <summary>
/// Sprite 预制体批量生成工具 —— EditorWindow 版本。
/// 从选中的图片自动创建包含主 Sprite 和 EchoOutline 子物体的预制体。
/// 菜单路径:Tools > Sprite预制体生成器
/// </summary>
public class SpritePrefabGeneratorWindow : EditorWindow
{
// ======================== 序列化字段 ========================
[SerializeField] private List<Sprite> selectedSprites = new List<Sprite>();
[SerializeField] private string outputPath = "Assets/Prefabs/Generated";
// 材质路径常量
private const string GroundClipMatPath = "Assets/Materials/cloud1_GroundClipMat.mat";
private const string EchoOutlineMatPath = "Assets/Light/shaders/IndianOcean_SpriteEchoOutline.mat";
// UI 状态
private Vector2 _scrollPos;
private List<string> _logMessages = new List<string>();
// 缓存的居中灰色标签样式
private GUIStyle _centeredGreyStyle;
private GUIStyle CenteredGreyStyle
{
get
{
if (_centeredGreyStyle == null)
{
_centeredGreyStyle = new GUIStyle(EditorStyles.miniLabel);
_centeredGreyStyle.alignment = TextAnchor.MiddleCenter;
_centeredGreyStyle.normal.textColor = new Color(0.5f, 0.5f, 0.5f);
}
return _centeredGreyStyle;
}
}
// ======================== 打开窗口 ========================
[MenuItem("Tools/Sprite预制体生成器")]
public static void ShowWindow()
{
var win = GetWindow<SpritePrefabGeneratorWindow>("Sprite预制体生成器");
win.minSize = new Vector2(420, 520);
}
// ======================== GUI ========================
private void OnGUI()
{
_scrollPos = EditorGUILayout.BeginScrollView(_scrollPos);
// ---- 标题 ----
GUILayout.Label("Sprite 预制体批量生成器", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"将图片拖入下方区域,自动生成预制体。\n" +
"预制体结构:\n" +
" 父物体(图片名)→ SpriteRenderer + GroundClip材质\n" +
" 子物体(EchoOutline)→ SpriteRenderer + EchoOutline材质 + Collider",
MessageType.Info);
EditorGUILayout.Space(8);
// ---- Sprite 选择区 ----
GUILayout.Label("① 选择 Sprite", EditorStyles.boldLabel);
DrawDropArea();
DrawSpriteList();
EditorGUILayout.Space(8);
// ---- 输出路径 ----
GUILayout.Label("② 输出路径", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
outputPath = EditorGUILayout.TextField(outputPath);
if (GUILayout.Button("浏览", GUILayout.Width(50)))
{
string folder = EditorUtility.OpenFolderPanel("选择预制体输出文件夹", "Assets", "");
if (!string.IsNullOrEmpty(folder))
{
int idx = folder.IndexOf("Assets");
if (idx >= 0)
outputPath = folder.Substring(idx);
else
EditorUtility.DisplayDialog("错误", "请选择项目 Assets 目录下的文件夹", "确定");
}
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(8);
// ---- 材质预览 ----
GUILayout.Label("③ 材质引用(自动加载)", EditorStyles.boldLabel);
DrawMaterialPreview(GroundClipMatPath, "GroundClip 材质");
DrawMaterialPreview(EchoOutlineMatPath, "EchoOutline 材质");
EditorGUILayout.Space(12);
// ---- 生成按钮 ----
GUI.enabled = selectedSprites.Count > 0;
if (GUILayout.Button($"批量生成预制体 ({selectedSprites.Count})", GUILayout.Height(36)))
{
GenerateAll();
}
GUI.enabled = true;
EditorGUILayout.Space(8);
// ---- 日志 ----
if (_logMessages.Count > 0)
{
GUILayout.Label("日志", EditorStyles.boldLabel);
foreach (var msg in _logMessages)
EditorGUILayout.LabelField(msg, EditorStyles.miniLabel);
}
EditorGUILayout.EndScrollView();
}
// ======================== 拖放区域 ========================
private void DrawDropArea()
{
Rect dropRect = GUILayoutUtility.GetRect(0, 70, GUILayout.ExpandWidth(true));
GUI.Box(dropRect, GUIContent.none, EditorStyles.helpBox);
// 绘制提示文字
EditorGUI.LabelField(dropRect,
selectedSprites.Count == 0
? "将 Sprite / Texture 从 Project 拖入此处"
: $"已选择 {selectedSprites.Count} 个 Sprite(可继续拖入追加)",
CenteredGreyStyle);
// 处理拖放事件
EventType evtType = Event.current.type;
if (evtType == EventType.DragUpdated || evtType == EventType.DragPerform)
{
bool hasValid = false;
foreach (var obj in DragAndDrop.objectReferences)
{
if (obj is Sprite || obj is Texture2D)
{
hasValid = true;
break;
}
}
if (hasValid) DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
if (evtType == EventType.DragPerform)
{
DragAndDrop.AcceptDrag();
AddFromDragObjects();
}
Event.current.Use();
}
}
private void AddFromDragObjects()
{
foreach (var obj in DragAndDrop.objectReferences)
{
if (obj is Sprite sp)
{
if (!selectedSprites.Contains(sp))
selectedSprites.Add(sp);
}
else if (obj is Texture2D tex)
{
string path = AssetDatabase.GetAssetPath(tex);
// 加载该纹理下的所有 Sprite(支持 Multiple 模式)
var subs = AssetDatabase.LoadAllAssetsAtPath(path);
bool added = false;
foreach (var sub in subs)
{
if (sub is Sprite s && !selectedSprites.Contains(s))
{
selectedSprites.Add(s);
added = true;
}
}
// Single 模式:直接加载
if (!added)
{
var single = AssetDatabase.LoadAssetAtPath<Sprite>(path);
if (single != null && !selectedSprites.Contains(single))
selectedSprites.Add(single);
}
}
}
}
// ======================== Sprite 列表 ========================
private void DrawSpriteList()
{
if (selectedSprites.Count == 0) return;
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
if (GUILayout.Button("清空列表", GUILayout.Width(70)))
selectedSprites.Clear();
EditorGUILayout.EndHorizontal();
for (int i = selectedSprites.Count - 1; i >= 0; i--)
{
EditorGUILayout.BeginHorizontal(EditorStyles.helpBox);
// Sprite 缩略图
var sp = selectedSprites[i];
if (sp != null)
{
var preview = AssetPreview.GetMiniThumbnail(sp);
GUILayout.Label(preview, GUILayout.Width(20), GUILayout.Height(20));
GUILayout.Label(sp.name, GUILayout.ExpandWidth(true));
}
else
{
GUILayout.Label("(null)", GUILayout.ExpandWidth(true));
}
if (GUILayout.Button("×", GUILayout.Width(22)))
selectedSprites.RemoveAt(i);
EditorGUILayout.EndHorizontal();
}
}
// ======================== 材质预览 ========================
private void DrawMaterialPreview(string assetPath, string label)
{
var mat = AssetDatabase.LoadAssetAtPath<Material>(assetPath);
if (mat != null)
{
EditorGUILayout.ObjectField(label, mat, typeof(Material), false);
}
else
{
EditorGUILayout.HelpBox($"未找到材质:{assetPath}", MessageType.Warning);
}
}
// ======================== 批量生成 ========================
private void GenerateAll()
{
// 移除空项
selectedSprites.RemoveAll(s => s == null);
if (selectedSprites.Count == 0)
{
EditorUtility.DisplayDialog("提示", "请先选择至少一个 Sprite", "确定");
return;
}
// 加载材质
Material groundClipMat = AssetDatabase.LoadAssetAtPath<Material>(GroundClipMatPath);
Material echoOutlineMat = AssetDatabase.LoadAssetAtPath<Material>(EchoOutlineMatPath);
if (groundClipMat == null)
{
EditorUtility.DisplayDialog("错误", $"找不到材质:{GroundClipMatPath}", "确定");
return;
}
if (echoOutlineMat == null)
{
EditorUtility.DisplayDialog("错误", $"找不到材质:{EchoOutlineMatPath}", "确定");
return;
}
// 确保输出目录存在
EnsureFolderExists(outputPath);
_logMessages.Clear();
int count = 0;
GameObject lastPrefab = null;
int total = selectedSprites.Count;
for (int idx = 0; idx < total; idx++)
{
var sprite = selectedSprites[idx];
if (EditorUtility.DisplayCancelableProgressBar("生成预制体",
$"正在生成 {sprite.name}... ({idx + 1}/{total})", (float)idx / total))
break;
var prefab = GenerateSingle(sprite, groundClipMat, echoOutlineMat);
if (prefab != null)
{
count++;
lastPrefab = prefab;
}
}
EditorUtility.ClearProgressBar();
// 保存并刷新
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
_logMessages.Add($"✓ 共生成 {count} 个预制体 → {outputPath}");
Debug.Log($"[SpritePrefabGenerator] 共生成 {count} 个预制体");
if (lastPrefab != null)
{
Selection.activeObject = lastPrefab;
EditorGUIUtility.PingObject(lastPrefab);
}
Repaint();
if (count > 0)
EditorUtility.DisplayDialog("完成", $"已生成 {count} 个预制体\n路径:{outputPath}", "确定");
}
// ======================== 生成单个预制体 ========================
private GameObject GenerateSingle(Sprite sprite, Material groundClipMat, Material echoOutlineMat)
{
// 清理文件名中的非法字符
string spriteName = SanitizeFileName(sprite.name);
string prefabPath = $"{outputPath}/{spriteName}.prefab";
// ---- 父物体 ----
GameObject root = new GameObject(sprite.name); // GameObject 名用原始名
root.transform.localPosition = Vector3.zero;
root.transform.localRotation = Quaternion.identity;
root.transform.localScale = Vector3.one;
// 父物体 SpriteRenderer
var parentSR = root.AddComponent<SpriteRenderer>();
parentSR.sprite = sprite;
parentSR.sharedMaterial = groundClipMat;
parentSR.sortingOrder = 1;
// ---- 子物体 EchoOutline ----
GameObject child = new GameObject("EchoOutline");
child.transform.SetParent(root.transform, false);
child.transform.localPosition = Vector3.zero;
child.transform.localRotation = Quaternion.identity;
child.transform.localScale = Vector3.one;
// 子物体 SpriteRenderer
var childSR = child.AddComponent<SpriteRenderer>();
childSR.sprite = sprite;
childSR.sharedMaterial = echoOutlineMat;
childSR.sortingOrder = 10001;
// 子物体 Collider(根据 Sprite Pivot 自动设置)
SetupCollider2D(child, sprite);
// 保存为预制体并验证结果
bool success;
PrefabUtility.SaveAsPrefabAsset(root, prefabPath, out success);
Object.DestroyImmediate(root);
if (success)
{
var asset = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
_logMessages.Add($"✓ {spriteName}.prefab");
Debug.Log($"[SpritePrefabGenerator] ✓ 已创建预制体:{prefabPath}");
return asset;
}
else
{
_logMessages.Add($"✗ {spriteName}.prefab 保存失败");
Debug.LogWarning($"[SpritePrefabGenerator] ✗ 保存失败:{prefabPath}");
return null;
}
}
// ======================== Collider 设置 ========================
/// <summary>
/// 根据 Sprite 的 Pivot 自动设置 2D Collider。
/// 优先使用 Sprite Physics Shape(精灵物理轮廓),
/// 若无则使用 Sprite 边界矩形(考虑 Pivot 偏移)。
/// </summary>
private void SetupCollider2D(GameObject go, Sprite sprite)
{
float ppu = sprite.pixelsPerUnit;
Vector2 pivot = sprite.pivot; // 像素坐标(用于 BoxCollider fallback
// 尝试使用 Sprite Physics Shape
// 注意:GetPhysicsShape 返回的坐标已经是像素单位且相对于 Pivot,
// 因此只需除以 PPU 即可转为世界单位
List<Vector2[]> paths = GetSpritePhysicsShape(sprite);
if (paths != null && paths.Count > 0)
{
var poly = go.AddComponent<PolygonCollider2D>();
poly.pathCount = paths.Count;
for (int i = 0; i < paths.Count; i++)
{
var worldPoints = new Vector2[paths[i].Length];
for (int j = 0; j < paths[i].Length; j++)
{
// Physics shape 坐标 = 像素、相对 pivot → 除以 PPU 得到世界单位
worldPoints[j] = paths[i][j] / ppu;
}
poly.SetPath(i, worldPoints);
}
}
else
{
// Fallback:基于 Sprite 边界矩形 + Pivot 偏移
var box = go.AddComponent<BoxCollider2D>();
Vector2 sizePx = new Vector2(sprite.rect.width, sprite.rect.height);
box.size = sizePx / ppu;
box.offset = (sizePx * 0.5f - pivot) / ppu;
}
}
/// <summary>
/// 读取 Sprite 的物理形状(Physics Shape),返回路径列表。
/// </summary>
private List<Vector2[]> GetSpritePhysicsShape(Sprite sprite)
{
var result = new List<Vector2[]>();
int pathCount = sprite.GetPhysicsShapeCount();
for (int i = 0; i < pathCount; i++)
{
var points = new List<Vector2>();
sprite.GetPhysicsShape(i, points);
if (points.Count >= 3)
result.Add(points.ToArray());
}
return result;
}
/// <summary>
/// 清理文件名中的非法字符(/\:*?"&lt;&gt;|)。
/// </summary>
private static string SanitizeFileName(string name)
{
foreach (char c in System.IO.Path.GetInvalidFileNameChars())
name = name.Replace(c, '_');
return name;
}
// ======================== 工具方法 ========================
/// <summary>
/// 确保 Asset 目录链存在(递归创建)。
/// </summary>
private void EnsureFolderExists(string folderPath)
{
if (AssetDatabase.IsValidFolder(folderPath)) return;
string parent = System.IO.Path.GetDirectoryName(folderPath).Replace('\\', '/');
if (!string.IsNullOrEmpty(parent) && !AssetDatabase.IsValidFolder(parent))
EnsureFolderExists(parent);
string folderName = System.IO.Path.GetFileName(folderPath);
AssetDatabase.CreateFolder(parent, folderName);
}
}