using UnityEngine; using UnityEditor; using System.Collections.Generic; /// /// Sprite 预制体批量生成工具 —— EditorWindow 版本。 /// 从选中的图片自动创建包含主 Sprite 和 EchoOutline 子物体的预制体。 /// 菜单路径:Tools > Sprite预制体生成器 /// public class SpritePrefabGeneratorWindow : EditorWindow { // ======================== 序列化字段 ======================== [SerializeField] private List selectedSprites = new List(); [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 _logMessages = new List(); // 缓存的居中灰色标签样式 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("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(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(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(GroundClipMatPath); Material echoOutlineMat = AssetDatabase.LoadAssetAtPath(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(); 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(); 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(prefabPath); _logMessages.Add($"✓ {spriteName}.prefab"); Debug.Log($"[SpritePrefabGenerator] ✓ 已创建预制体:{prefabPath}"); return asset; } else { _logMessages.Add($"✗ {spriteName}.prefab 保存失败"); Debug.LogWarning($"[SpritePrefabGenerator] ✗ 保存失败:{prefabPath}"); return null; } } // ======================== Collider 设置 ======================== /// /// 根据 Sprite 的 Pivot 自动设置 2D Collider。 /// 优先使用 Sprite Physics Shape(精灵物理轮廓), /// 若无则使用 Sprite 边界矩形(考虑 Pivot 偏移)。 /// private void SetupCollider2D(GameObject go, Sprite sprite) { float ppu = sprite.pixelsPerUnit; Vector2 pivot = sprite.pivot; // 像素坐标(用于 BoxCollider fallback) // 尝试使用 Sprite Physics Shape // 注意:GetPhysicsShape 返回的坐标已经是像素单位且相对于 Pivot, // 因此只需除以 PPU 即可转为世界单位 List paths = GetSpritePhysicsShape(sprite); if (paths != null && paths.Count > 0) { var poly = go.AddComponent(); 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(); Vector2 sizePx = new Vector2(sprite.rect.width, sprite.rect.height); box.size = sizePx / ppu; box.offset = (sizePx * 0.5f - pivot) / ppu; } } /// /// 读取 Sprite 的物理形状(Physics Shape),返回路径列表。 /// private List GetSpritePhysicsShape(Sprite sprite) { var result = new List(); int pathCount = sprite.GetPhysicsShapeCount(); for (int i = 0; i < pathCount; i++) { var points = new List(); sprite.GetPhysicsShape(i, points); if (points.Count >= 3) result.Add(points.ToArray()); } return result; } /// /// 清理文件名中的非法字符(/\:*?"<>|)。 /// private static string SanitizeFileName(string name) { foreach (char c in System.IO.Path.GetInvalidFileNameChars()) name = name.Replace(c, '_'); return name; } // ======================== 工具方法 ======================== /// /// 确保 Asset 目录链存在(递归创建)。 /// 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); } }