添加新脚本

This commit is contained in:
JA
2026-07-01 14:27:57 +08:00
parent cf9f694615
commit 969e1bf6ba
86 changed files with 12435 additions and 28 deletions
@@ -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>
/// 清理文件名中的非法字符(/\:*?"&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);
}
}