添加新脚本
This commit is contained in:
@@ -0,0 +1,451 @@
|
||||
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>
|
||||
/// 清理文件名中的非法字符(/\:*?"<>|)。
|
||||
/// </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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user