特效处理

This commit is contained in:
2026-10-20 18:30:03 +08:00
parent 40bd235e67
commit bee2d42356
131 changed files with 837 additions and 456 deletions
@@ -1,9 +0,0 @@
fileFormatVersion: 2
guid: e3371e9602fc12543b4026c95d444839
folderAsset: yes
timeCreated: 1501134707
licenseType: Free
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:
@@ -1,211 +0,0 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.Linq;
using System.IO;
using System.Threading.Tasks;
/// <summary>
/// 保存需要烘焙的动画的相关数据
/// </summary>
public struct AnimData
{
#region 字段
public int vertexCount;
public int mapWidth;
public List<AnimationState> animClips;
public string name;
private Animation animation;
private SkinnedMeshRenderer skin;
#endregion
public AnimData(Animation anim, SkinnedMeshRenderer smr, string goName)
{
vertexCount = smr.sharedMesh.vertexCount;
mapWidth = Mathf.NextPowerOfTwo(vertexCount);
animClips = new List<AnimationState>(anim.Cast<AnimationState>());
animation = anim;
skin = smr;
name = goName;
}
#region 方法
public void AnimationPlay(string animName)
{
this.animation.Play(animName);
}
public void SampleAnimAndBakeMesh(ref Mesh m)
{
this.SampleAnim();
this.BakeMesh(ref m);
}
private void SampleAnim()
{
if (this.animation == null)
{
Debug.LogError("animation is null!!");
return;
}
this.animation.Sample();
}
private void BakeMesh(ref Mesh m)
{
if (this.skin == null)
{
Debug.LogError("skin is null!!");
return;
}
this.skin.BakeMesh(m);
}
#endregion
}
/// <summary>
/// 烘焙后的数据
/// </summary>
public struct BakedData
{
#region 字段
public string name;
public float animLen;
public byte[] rawAnimMap;
public int animMapWidth;
public int animMapHeight;
#endregion
public BakedData(string name, float animLen, Texture2D animMap)
{
this.name = name;
this.animLen = animLen;
this.animMapHeight = animMap.height;
this.animMapWidth = animMap.width;
this.rawAnimMap = animMap.GetRawTextureData();
}
}
/// <summary>
/// 烘焙器
/// </summary>
public class AnimMapBaker{
#region 字段
private AnimData? animData = null;
private List<Vector3> vertices = new List<Vector3>();
private Mesh bakedMesh;
private List<BakedData> bakedDataList = new List<BakedData>();
#endregion
#region 方法
public void SetAnimData(GameObject go)
{
if(go == null)
{
Debug.LogError("go is null!!");
return;
}
Animation anim = go.GetComponent<Animation>();
SkinnedMeshRenderer smr = go.GetComponentInChildren<SkinnedMeshRenderer>();
if(anim == null || smr == null)
{
Debug.LogError("anim or smr is null!!");
return;
}
this.bakedMesh = new Mesh();
this.animData = new AnimData(anim, smr, go.name);
}
int animNum;
public List<BakedData> Bake()
{
if(this.animData == null)
{
Debug.LogError("bake data is null!!");
return this.bakedDataList;
}
//每一个动作都生成一个动作图
for(int i = 0; i < this.animData.Value.animClips.Count; i++)
{
animNum = i;
if (!this.animData.Value.animClips[i].clip.legacy)
{
Debug.LogError(string.Format("{0} is not legacy!!", this.animData.Value.animClips[i].clip.name));
continue;
}
BakePerAnimClip(this.animData.Value.animClips[i]);
}
return this.bakedDataList;
}
private void BakePerAnimClip(AnimationState curAnim)
{
int curClipFrame = 0;
float sampleTime = 0;
float perFrameTime = 0;
curClipFrame = Mathf.ClosestPowerOfTwo((int)(curAnim.clip.frameRate * curAnim.length));
perFrameTime = curAnim.length / curClipFrame; ;
Texture2D animMap = new Texture2D(this.animData.Value.mapWidth, curClipFrame, TextureFormat.RGBAHalf, false);
animMap.name = string.Format("{0}_{1}.animMap", this.animData.Value.name, curAnim.name);
this.animData.Value.AnimationPlay(curAnim.name);
for (int i = 0; i < curClipFrame; i++)
{
//EditorUtility.DisplayCancelableProgressBar("转换进度", $"正在处理第 {i}/{curClipFrame} 步", (float)i / curClipFrame);
curAnim.time = sampleTime;
this.animData.Value.SampleAnimAndBakeMesh(ref this.bakedMesh);
for(int j = 0; j < this.bakedMesh.vertexCount; j++)
{
EditorUtility.DisplayCancelableProgressBar("转换进度", $"正在处理第 {i}/{curClipFrame} 步 {j}/{bakedMesh.vertexCount}点", (float)i / curClipFrame);
Vector3 vertex = this.bakedMesh.vertices[j];
animMap.SetPixel(j, i, new Color(vertex.x, vertex.y, vertex.z));
}
sampleTime += perFrameTime;
}
EditorUtility.ClearProgressBar();
animMap.Apply();
Debug.Log($"{animNum}/{animData.Value.animClips.Count-1}");
if (animNum== animData.Value.animClips.Count - 1)
{
Debug.Log("转换完成");
}
this.bakedDataList.Add(new BakedData(animMap.name, curAnim.clip.length, animMap));
}
#endregion
#region 属性
#endregion
}
@@ -1,12 +0,0 @@
fileFormatVersion: 2
guid: d58f874be9d391e43866e5e334ddf986
timeCreated: 1501134718
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,174 +0,0 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.IO;
public class AnimMapBakerWindow : EditorWindow {
private enum SaveStrategy
{
AnimMap,//only anim map
Mat,//with shader
Prefab//prefab with mat
}
#region 字段
public static GameObject targetGo;
private static AnimMapBaker baker;
private static string path = "GamePlay_ECS\\EntityConvert";
private static string subPath = "SubPath";
private static SaveStrategy stratege = SaveStrategy.AnimMap;
private static Shader _animMapShader;
private static Shader animMapShader
{
get
{
if(_animMapShader == null)
_animMapShader = Shader.Find(animMapShaderName);
return _animMapShader;
}
}
private static string animMapShaderName = "ECSGame/URP/UnlitAnim";
#endregion
#region 方法
[MenuItem("Tools/AnimMapBaker")]
public static void ShowWindow()
{
EditorWindow.GetWindow(typeof(AnimMapBakerWindow));
baker = new AnimMapBaker();
}
void OnGUI()
{
targetGo = (GameObject)EditorGUILayout.ObjectField(targetGo, typeof(GameObject), true);
subPath = targetGo == null ? subPath : targetGo.name;
EditorGUILayout.LabelField(string.Format("保存路径output path:{0}", Path.Combine(path, subPath)));
path = EditorGUILayout.TextField(path);
subPath = EditorGUILayout.TextField(subPath);
stratege = (SaveStrategy)EditorGUILayout.EnumPopup("保存策略output type:", stratege);
if (GUILayout.Button("Bake"))
{
if(targetGo == null)
{
EditorUtility.DisplayDialog("err", "targetGo is null!", "OK");
return;
}
if(baker == null)
{
baker = new AnimMapBaker();
}
baker.SetAnimData(targetGo);
List<BakedData> list =baker.Bake();
if (list != null)
{
for(int i = 0; i < list.Count; i++)
{
BakedData data = list[i];
Save(ref data);
}
}
}
}
private void Save(ref BakedData data)
{
switch(stratege)
{
case SaveStrategy.AnimMap:
SaveAsAsset(ref data);
break;
case SaveStrategy.Mat:
SaveAsMat(ref data);
break;
case SaveStrategy.Prefab:
SaveAsPrefab(ref data);
break;
}
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
private Texture2D SaveAsAsset(ref BakedData data)
{
string folderPath = CreateFolder();
Texture2D animMap = new Texture2D(data.animMapWidth, data.animMapHeight, TextureFormat.RGBAHalf, false);
animMap.LoadRawTextureData(data.rawAnimMap);
AssetDatabase.CreateAsset(animMap, Path.Combine(folderPath, GetValidName(data.name) + ".asset"));
return animMap;
}
public static string GetValidName(string pName)
{
pName = pName.Replace("|", "@");
return pName;
}
private Material SaveAsMat(ref BakedData data)
{
if(animMapShader == null)
{
EditorUtility.DisplayDialog("err", "shader is null!!", "OK");
return null;
}
if(targetGo == null || !targetGo.GetComponentInChildren<SkinnedMeshRenderer>())
{
EditorUtility.DisplayDialog("err", "SkinnedMeshRender is null!!", "OK");
return null;
}
SkinnedMeshRenderer smr = targetGo.GetComponentInChildren<SkinnedMeshRenderer>();
Material mat = new Material(animMapShader);
Texture2D animMap = SaveAsAsset(ref data);
mat.SetTexture("_MainTex", smr.sharedMaterial.mainTexture);
mat.SetTexture("_AnimMap", animMap);
mat.SetFloat("_AnimLen", data.animLen);
string folderPath = CreateFolder();
AssetDatabase.CreateAsset(mat, Path.Combine(folderPath, GetValidName(data.name) + ".mat"));
return mat;
}
private void SaveAsPrefab(ref BakedData data)
{
Material mat = SaveAsMat(ref data);
if(mat == null)
{
EditorUtility.DisplayDialog("err", "mat is null!!", "OK");
return;
}
GameObject go = new GameObject();
go.AddComponent<MeshRenderer>().sharedMaterial = mat;
go.AddComponent<MeshFilter>().sharedMesh = targetGo.GetComponentInChildren<SkinnedMeshRenderer>().sharedMesh;
string folderPath = CreateFolder();
PrefabUtility.CreatePrefab(Path.Combine(folderPath, GetValidName(data.name) + ".prefab").Replace("\\", "/"), go);
}
private string CreateFolder()
{
string folderPath = Path.Combine("Assets/" + path, subPath);
if (!AssetDatabase.IsValidFolder(folderPath))
{
AssetDatabase.CreateFolder("Assets/" + path, subPath);
}
return folderPath;
}
#endregion
}
@@ -1,12 +0,0 @@
fileFormatVersion: 2
guid: b0821312d64995540ae7372c5af049c0
timeCreated: 1501143281
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,28 +0,0 @@
{
"name": "Game.Hotfix.Editor.AnimMapBaker",
"rootNamespace": "",
"references": [
"GameFramework",
"Game.Hotfix",
"CameraShake",
"spine-unity",
"Coffee.UIParticle",
"Unity.Addressables",
"Unity.ResourceManager",
"Unity.TextMeshPro",
"UnityEngine.UI",
"Unity.InputSystem",
"Unity.VisualScripting.Core"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": true,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": false,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 9dc34cc9588f7c04d887b97a4747f393
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 9c5dd14d58b09344e8068bd676a4ec9d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
File diff suppressed because it is too large Load Diff
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: de66b70d0c2dd9d4aa8cc22dfe238d4a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,529 +0,0 @@
using UnityEngine;
using UnityEditor;
using System.IO;
using System.Collections.Generic;
using System.Linq;
public class AnimationClipCreator2D : EditorWindow
{
private Vector2 scrollDirectoryPos; //文件夹滚动条
private Vector2 scrollAnimationClipPos; //动画滚动条
// 全局设置
private string rootFolderPath = "Assets/SpriteGroups"; // 源根目录
private string saveFolderPath = "Assets/Animations"; // 输出根目录
private string fixedSpritePrefix = "Sprite"; // 固定序列帧前缀
private int defaultNumCount = 5; // 序号补零位数
private bool showItems = true;
//// 已发现的子文件夹
//private List<string> subFolders = new List<string>();
//private List<string> subFolderNames = new List<string>();
//private List<bool> subFolderToggles = new List<bool>();
// 动画条目
[System.Serializable]
private class AnimItem
{
public string animName = "Idle";
public int startFrame = 0;
public int endFrame = 10;
public float frameRate = 12f;
public bool overrideNumCount = false;
public int numCount = 5;
}
private List<AnimItem> items = new List<AnimItem>()
{
new AnimItem(){ animName="Idle", startFrame=0, endFrame=10, frameRate=12f },
new AnimItem(){ animName="Run", startFrame=0, endFrame=7, frameRate=12f },
};
// EditorPrefs keys
private const string PREF_ROOT = "AnimCreator_RootPath";
private const string PREF_SAVE = "AnimCreator_SavePath";
private const string PREF_PREFIX = "AnimCreator_FixedPrefix";
private const string PREF_NUMCOUNT = "AnimCreator_NumCount";
[MenuItem("Tools/2D Animation Clip Creator (Folders)")]
public static void ShowWindow()
{
var window = GetWindow<AnimationClipCreator2D>("Animation Clip Creator");
window.LoadPrefs();
window.LoadAnimItems(); // 加载动画条目
}
private void OnGUI()
{
GUILayout.Label("批量创建 AnimationClip(子文件夹驱动,固定 PNG 前缀)", EditorStyles.boldLabel);
rootFolderPath = EditorGUILayout.TextField("序列帧根目录", rootFolderPath);
saveFolderPath = EditorGUILayout.TextField("输出动画根目录", saveFolderPath);
fixedSpritePrefix = EditorGUILayout.TextField("固定序列帧前缀", fixedSpritePrefix);
defaultNumCount = EditorGUILayout.IntField("默认补零位数", defaultNumCount);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("刷新子文件夹"))
{
RefreshFolderData();
}
EditorGUILayout.LabelField($"已发现子文件夹数量: {folderHasImages.Count}");
EditorGUILayout.EndHorizontal();
GUILayout.Label("选择要生成的子文件夹:", EditorStyles.boldLabel);
// 模式切换选项
displayMode = (DisplayMode)EditorGUILayout.EnumPopup("显示模式", displayMode);
// 子文件夹选择列表
scrollDirectoryPos = EditorGUILayout.BeginScrollView(scrollDirectoryPos, GUILayout.MaxHeight(400));
if (folderHasImages.Count > 0)
{
GUILayout.Space(10);
if (displayMode == DisplayMode.Grid)
{
DrawGridMode();
}
else
{
DrawTreeMode();
}
}
EditorGUILayout.EndScrollView();
// 动画条目列表
showItems = EditorGUILayout.BeginFoldoutHeaderGroup(showItems, $"动画条目列表 ({items.Count})");
scrollAnimationClipPos = EditorGUILayout.BeginScrollView(scrollAnimationClipPos);
if (showItems)
{
for (int i = 0; i < items.Count; i++)
{
var it = items[i];
EditorGUILayout.BeginVertical("box");
EditorGUILayout.BeginHorizontal();
it.animName = EditorGUILayout.TextField("动画名字", it.animName);
if (GUILayout.Button("删除", GUILayout.Width(60)))
{
items.RemoveAt(i);
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
break;
}
EditorGUILayout.EndHorizontal();
it.startFrame = EditorGUILayout.IntField("开始帧", it.startFrame);
it.endFrame = EditorGUILayout.IntField("结束帧(含)", it.endFrame);
it.frameRate = EditorGUILayout.FloatField("帧率", it.frameRate);
it.overrideNumCount = EditorGUILayout.Toggle("覆盖补零位数", it.overrideNumCount);
if (it.overrideNumCount)
it.numCount = EditorGUILayout.IntField("补零位数", it.numCount);
// 预览:用第一个子文件夹示例
if (folderHasImages.Count > 0)
{
string folderName = "";
foreach (var item in folderHasImages.Keys)
{
folderName=item;
}
int nc = it.overrideNumCount ? it.numCount : defaultNumCount;
string sampleIndex = it.startFrame.ToString("D" + nc);
string samplePng = $"{fixedSpritePrefix}{sampleIndex}.png";
string sampleOut = $"{Path.GetFileName(folderName)}_{it.animName}.anim";
EditorGUILayout.HelpBox(
$"示例 PNG 路径:\n{rootFolderPath}/{folderName}/{samplePng}\n" +
$"输出动画文件名:\n{sampleOut}\n" +
$"输出动画完整路径:\n{saveFolderPath}/{folderName}/{sampleOut}",
MessageType.None
);
}
EditorGUILayout.EndVertical();
}
if (GUILayout.Button("添加新动画条目"))
{
items.Add(new AnimItem()
{
animName = "NewAnim",
startFrame = 0,
endFrame = 10,
frameRate = 12f,
overrideNumCount = false,
numCount = defaultNumCount
});
}
}
EditorGUILayout.EndFoldoutHeaderGroup();
EditorGUILayout.EndScrollView();
GUILayout.Space(8);
if (GUILayout.Button("批量创建 AnimationClips", GUILayout.Height(30)))
{
SavePrefs();
SaveAnimItems();
CreateAnimationClips();
}
}
// 定义模式枚举
private enum DisplayMode { Grid, Tree }
private DisplayMode displayMode = DisplayMode.Grid;
// 状态字典(按完整路径存)
private Dictionary<string, bool> folderToggles = new Dictionary<string, bool>(); //目录的toggle值
private Dictionary<string, bool> folderHasImages = new Dictionary<string, bool>(); //目录是否带图片
private Dictionary<string, string> folderAsSubName = new Dictionary<string, string>(); //末端目录名称
#region 树形
private Dictionary<string, bool> folderFoldouts = new Dictionary<string, bool>(); //文件夹展开状态
// 递归刷新:构建状态字典(包含根)
private void RefreshFolderData()
{
folderFoldouts.Clear();
folderAsSubName.Clear();
folderToggles.Clear();
folderHasImages.Clear();
if (!Directory.Exists(rootFolderPath))
{
EditorUtility.DisplayDialog("错误", "根目录不存在!", "确定");
return;
}
// 加入根目录本身
EnsureNode(rootFolderPath);
// 递归加入所有子目录
CollectNodesRecursive(rootFolderPath);
Repaint();
}
#endregion
// 平铺模式(横向自动换行)
private void DrawGridMode()
{
if (folderHasImages.Count > 0)
{
float viewWidth = EditorGUIUtility.currentViewWidth - 20;
float itemWidth = 140f;
float spacing = 10f;
float currentRowWidth = 0f;
EditorGUILayout.BeginVertical();
EditorGUILayout.BeginHorizontal();
foreach (var folder in folderHasImages.Keys) //随便找一个列表的键来便利所有的目录
{
if (currentRowWidth + (itemWidth + spacing) > viewWidth)
{
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
currentRowWidth = 0f;
}
string folderPath = folder;
//folderToggles[folderPath] = EditorGUILayout.ToggleLeft(subFolderNames[i], folderToggles[folderPath]);
EditorGUILayout.BeginVertical("box", GUILayout.Width(itemWidth));
folderToggles[folderPath] = EditorGUILayout.ToggleLeft(Path.GetFileName(folderPath), folderToggles[folderPath]);
//subFolderToggles[i] = EditorGUILayout.ToggleLeft(subFolderNames[i], subFolderToggles[i]);
string status = folderHasImages[folderPath] ? " 有图片" : " 无图片";
EditorGUILayout.LabelField(status, EditorStyles.wordWrappedLabel);
EditorGUILayout.EndVertical();
currentRowWidth += itemWidth + spacing;
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
}
}
// 将一个目录加入字典(若已存在则保留原值)
private void EnsureNode(string path)
{
string norm = path.Replace('\\', '/');
if (!folderFoldouts.ContainsKey(norm))
folderFoldouts[norm] = true; // 默认展开
if (!folderHasImages.ContainsKey(norm))
{
// 当前目录是否有 .png
bool hasPng = Directory.GetFiles(norm, "*.png").Length > 0;
folderHasImages[norm] = hasPng;
}
string fileName = Path.GetFileName(norm);
if (!folderAsSubName.ContainsKey(fileName))
folderAsSubName[norm] = fileName; // 有图默认勾选,无图默认不勾选
if (!folderToggles.ContainsKey(norm))
folderToggles[norm] = folderHasImages[norm]; // 有图默认勾选,无图默认不勾选
}
private void CollectNodesRecursive(string current)
{
var dirs = Directory.GetDirectories(current);
foreach (var d in dirs)
{
string norm = d.Replace('\\', '/');
EnsureNode(norm);
CollectNodesRecursive(norm);
}
}
// 树形绘制(从根开始)
private void DrawTreeMode()
{
if (!Directory.Exists(rootFolderPath))
{
EditorGUILayout.HelpBox("根目录不存在", MessageType.Error);
return;
}
GUILayout.Label("选择要生成的子文件夹:", EditorStyles.boldLabel);
DrawFolderTreeNode(rootFolderPath, 0);
}
// 单节点绘制(折叠箭头 + 名字 + Toggle + 状态),递归子节点
private void DrawFolderTreeNode(string path, int depth)
{
string norm = path.Replace('\\', '/');
string name = Path.GetFileName(norm);
if (string.IsNullOrEmpty(name)) name = norm; // 根可能没有文件名
// 保证节点存在
EnsureNode(norm);
EditorGUILayout.BeginHorizontal();
GUILayout.Space(depth * 18f); // 缩进
// 折叠箭头 + 名字(true 参数启用丰富样式)
folderFoldouts[norm] = EditorGUILayout.Foldout(folderFoldouts[norm], name, true);
// Toggle(同一行显示,宽度固定避免布局抖动)
folderToggles[norm] = EditorGUILayout.Toggle(folderToggles[norm], GUILayout.Width(20));
// 状态文本(自动换行)
string status = folderHasImages[norm] ? " 有图片" : " 无图片";
EditorGUILayout.LabelField(status, EditorStyles.wordWrappedLabel);
EditorGUILayout.EndHorizontal();
// 展开时绘制子目录
if (folderFoldouts[norm])
{
var dirs = Directory.GetDirectories(norm);
foreach (var d in dirs)
{
DrawFolderTreeNode(d, depth + 1);
}
}
}
private void OnDisable()
{
SavePrefs();
SaveAnimItems();
}
private const string PREF_ITEMS = "AnimCreator_AnimItems";
// 保存动画条目列表
private void SaveAnimItems()
{
string json = JsonUtility.ToJson(new AnimItemListWrapper { items = this.items });
EditorPrefs.SetString(PREF_ITEMS, json);
}
// 加载动画条目列表
private void LoadAnimItems()
{
if (EditorPrefs.HasKey(PREF_ITEMS))
{
string json = EditorPrefs.GetString(PREF_ITEMS);
var wrapper = JsonUtility.FromJson<AnimItemListWrapper>(json);
if (wrapper != null && wrapper.items != null)
this.items = wrapper.items;
}
}
// 包装类用于序列化 List<AnimItem>
[System.Serializable]
private class AnimItemListWrapper
{
public List<AnimItem> items;
}
private void CreateAnimationClips()
{
if (folderHasImages.Count == 0)
{
EditorUtility.DisplayDialog("错误", "没有发现子文件夹!", "确定");
return;
}
// 1. 先筛选出所有选中的目录(避免循环中重复判断)
var selectedFolders = folderToggles.Where(kv => kv.Value).Select(kv => kv.Key).ToList();
if (selectedFolders.Count == 0)
{
EditorUtility.DisplayDialog("提示", "请先选中要处理的文件夹!", "确定");
return;
}
// 2. 计算总任务量(选中目录数 × 每个目录的动画数),让进度更平滑
int totalTasks = selectedFolders.Count * items.Count;
int completedTasks = 0;
int created = 0;
try
{
foreach (var folderPath in selectedFolders)
{
string relativePath = folderPath.Substring(rootFolderPath.Length + 1);
string outFolder = Path.Combine(saveFolderPath, relativePath).Replace('\\', '/');
// 创建输出目录(优化:统一刷新,避免多次调用 AssetDatabase.Refresh())
if (!Directory.Exists(outFolder))
{
Directory.CreateDirectory(outFolder);
}
foreach (var it in items)
{
// 3. 支持取消操作(用户点击进度条的“取消”按钮)
if (EditorUtility.DisplayCancelableProgressBar(
"批量创建动画",
$"正在处理:{folderPath}\n动画:{it.animName}\n进度:{completedTasks + 1}/{totalTasks}",
(float)completedTasks / totalTasks))
{
EditorUtility.DisplayDialog("提示", "操作已取消!", "确定");
return; // 退出方法,不再继续处理
}
// 跳过无效帧范围的动画
if (it.endFrame < it.startFrame)
{
Debug.LogError($"动画 {it.animName} 的结束帧必须 >= 开始帧,跳过。");
completedTasks++; // 即使跳过,也计入已完成任务量(避免进度卡住)
continue;
}
int numCount = it.overrideNumCount ? it.numCount : defaultNumCount;
var clip = new AnimationClip { frameRate = Mathf.Max(1f, it.frameRate) };
var binding = new EditorCurveBinding
{
type = typeof(SpriteRenderer),
path = "",
propertyName = "m_Sprite"
};
int len = it.endFrame - it.startFrame + 1;
var keyframes = new ObjectReferenceKeyframe[len];
int validCount = 0;
for (int i = 0; i < len; i++)
{
int frameIndex = it.startFrame + i;
string indexStr = frameIndex.ToString("D" + numCount);
string pngName = $"{fixedSpritePrefix}{indexStr}.png";
string spritePath = $"{folderPath}/{pngName}";
Sprite sprite = AssetDatabase.LoadAssetAtPath<Sprite>(spritePath);
if (sprite == null)
{
Debug.LogWarning($"未找到图片: {spritePath}");
continue;
}
keyframes[i] = new ObjectReferenceKeyframe
{
time = i / clip.frameRate,
value = sprite
};
validCount++;
}
if (validCount == 0)
{
Debug.LogError($"动画 {it.animName} 在文件夹 {folderPath} 未找到任何有效序列帧,跳过。");
completedTasks++;
continue;
}
AnimationUtility.SetObjectReferenceCurve(clip, binding, keyframes);
// 优化输出文件名(避免路径过长,用相对路径简化)
string folderName = Path.GetFileName(folderPath);
string outName = $"{folderName}_{it.animName}.anim";
string outPath = Path.Combine(outFolder, outName).Replace('\\', '/');
// 确保路径以 Assets/ 开头(Unity 资源路径规范)
if (!outPath.StartsWith("Assets/"))
{
outPath = outPath.Replace(Application.dataPath, "Assets");
}
AssetDatabase.CreateAsset(clip, outPath);
created++;
completedTasks++; // 完成一个动画,更新任务计数
Debug.Log($"已创建动画片段:{outPath}");
}
}
// 统一刷新资源数据库(优化性能,避免多次刷新)
AssetDatabase.Refresh();
AssetDatabase.SaveAssets();
EditorUtility.DisplayDialog("完成", $"成功创建 {created} 个动画片段!", "确定");
}
catch (System.Exception e)
{
// 异常处理:提示错误并关闭进度条
EditorUtility.DisplayDialog("错误", $"创建动画失败:{e.Message}\n{e.StackTrace}", "确定");
}
finally
{
// 4. 必须关闭进度条(无论成功/失败/取消)
EditorUtility.ClearProgressBar();
}
}
// 保存设置到 EditorPrefs
private void SavePrefs()
{
EditorPrefs.SetString(PREF_ROOT, rootFolderPath);
EditorPrefs.SetString(PREF_SAVE, saveFolderPath);
EditorPrefs.SetString(PREF_PREFIX, fixedSpritePrefix);
EditorPrefs.SetInt(PREF_NUMCOUNT, defaultNumCount);
}
// 加载设置
private void LoadPrefs()
{
if (EditorPrefs.HasKey(PREF_ROOT)) rootFolderPath = EditorPrefs.GetString(PREF_ROOT);
if (EditorPrefs.HasKey(PREF_SAVE)) saveFolderPath = EditorPrefs.GetString(PREF_SAVE);
if (EditorPrefs.HasKey(PREF_PREFIX)) fixedSpritePrefix = EditorPrefs.GetString(PREF_PREFIX);
if (EditorPrefs.HasKey(PREF_NUMCOUNT)) defaultNumCount = EditorPrefs.GetInt(PREF_NUMCOUNT);
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 7783783f6a178d744b5598f36a97169b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,196 +0,0 @@
using UnityEngine;
using UnityEditor;
using UnityEditor.Animations;
using UnityEditorInternal;
using System.Collections.Generic;
using AnimatorController = UnityEditor.Animations.AnimatorController;
public class AnimatorLinkWindow : EditorWindow
{
private AnimatorController animatorController;
private List<AnimationClip> clips = new List<AnimationClip>();
private ReorderableList reorderableList;
// 排版参数
private int statesPerColumn = 5;
private float horizontalSpacing = 200f;
private float verticalSpacing = 80f;
[MenuItem("Tools/Animator Link Window")]
public static void ShowWindow()
{
GetWindow<AnimatorLinkWindow>("Animator Linker");
}
private void OnEnable()
{
reorderableList = new ReorderableList(clips, typeof(AnimationClip), true, true, true, true);
reorderableList.drawHeaderCallback = (Rect rect) =>
{
EditorGUI.LabelField(rect, "Animation Clips (可拖拽排序)");
};
reorderableList.drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) =>
{
clips[index] = (AnimationClip)EditorGUI.ObjectField(
new Rect(rect.x, rect.y, rect.width, EditorGUIUtility.singleLineHeight),
clips[index], typeof(AnimationClip), false);
};
reorderableList.onAddCallback = (ReorderableList list) =>
{
clips.Add(null);
};
reorderableList.onRemoveCallback = (ReorderableList list) =>
{
clips.RemoveAt(list.index);
};
}
private Vector2 scrollPos;
private void OnGUI()
{
GUILayout.Label("Animator Controller", EditorStyles.boldLabel);
animatorController = (AnimatorController)EditorGUILayout.ObjectField(
"Controller", animatorController, typeof(AnimatorController), false);
GUILayout.Space(10);
// 排版参数输入框
statesPerColumn = EditorGUILayout.IntField("每列多少个状态", statesPerColumn);
horizontalSpacing = EditorGUILayout.FloatField("同列水平间隔", horizontalSpacing);
verticalSpacing = EditorGUILayout.FloatField("列与列垂直间隔", verticalSpacing);
// 开始滚动区域
scrollPos = EditorGUILayout.BeginScrollView(scrollPos);
GUILayout.Space(10);
reorderableList.DoLayoutList();
EditorGUILayout.EndScrollView(); // 结束滚动区域
Rect dropArea = GUILayoutUtility.GetRect(0.0f, 50.0f, GUILayout.ExpandWidth(true));
GUI.Box(dropArea, "拖拽 AnimationClip 到这里 (支持多选)");
HandleDragAndDrop(dropArea);
GUILayout.Space(10);
if (GUILayout.Button("生成状态并连线"))
{
GenerateStatesAndTransitions();
}
}
private void HandleDragAndDrop(Rect dropArea)
{
Event evt = Event.current;
if (!dropArea.Contains(evt.mousePosition)) return;
switch (evt.type)
{
case EventType.DragUpdated:
case EventType.DragPerform:
if (DragAndDrop.objectReferences.Length > 0)
{
DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
if (evt.type == EventType.DragPerform)
{
DragAndDrop.AcceptDrag();
foreach (Object obj in DragAndDrop.objectReferences)
{
if (obj is AnimationClip clip)
{
clips.Add(clip);
}
}
Debug.Log($"已添加 {DragAndDrop.objectReferences.Length} 个动画");
}
}
Event.current.Use();
break;
}
}
private void GenerateStatesAndTransitions()
{
if (animatorController == null)
{
Debug.LogError("请先指定 AnimatorController");
return;
}
if (clips.Count == 0)
{
Debug.LogError("请至少添加一个 AnimationClip");
return;
}
var rootStateMachine = animatorController.layers[0].stateMachine;
// 判断是否已有状态
if (rootStateMachine.states.Length > 0)
{
int option = EditorUtility.DisplayDialogComplex(
"已有状态",
"当前 AnimatorController 已经有状态,是否清空后重新生成?",
"清空并重新生成",
"从尾部继续添加",
"取消"
);
if (option == 0)
{
// 清空
rootStateMachine.states = new ChildAnimatorState[0];
}
else if (option == 2)
{
// 取消
return;
}
// option == 1 表示从尾部继续添加,不清空
}
AnimatorState prevState = null;
if (rootStateMachine.states.Length > 0)
{
// 如果已有状态,从最后一个开始连线
prevState = rootStateMachine.states[rootStateMachine.states.Length - 1].state;
}
int existingCount = rootStateMachine.states.Length;
for (int i = 0; i < clips.Count; i++)
{
var clip = clips[i];
if (clip == null) continue;
// ✅ 计算偏移量:已有状态数量 + 新的索引
int index = existingCount + i;
int col = index / statesPerColumn;
int row = index % statesPerColumn;
var state = rootStateMachine.AddState(
clip.name,
new Vector3(col * horizontalSpacing, row * verticalSpacing, 0)
);
state.motion = clip;
if (prevState != null)
{
var transition = prevState.AddTransition(state);
transition.hasExitTime = true;
transition.exitTime = 1f;
transition.duration = 0f;
}
prevState = state;
}
Debug.Log("生成完成:已按顺序创建状态并排版连线");
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: e16de956fbdac1249b91b66baeece38b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,28 +0,0 @@
{
"name": "Game.Hotfix.Editor.Animation",
"rootNamespace": "",
"references": [
"GameFramework",
"Game.Hotfix",
"CameraShake",
"spine-unity",
"Coffee.UIParticle",
"Unity.Addressables",
"Unity.ResourceManager",
"Unity.TextMeshPro",
"UnityEngine.UI",
"Unity.InputSystem",
"Unity.VisualScripting.Core"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": true,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": false,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 590ad1417b4e4fd449f888bc3f2127a3
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 6bac26f79fb7db24381d8d1f9fab586b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,28 +0,0 @@
{
"name": "Game.Hotfix.Editor.MissingScript",
"rootNamespace": "",
"references": [
"GameFramework",
"Game.Hotfix",
"CameraShake",
"spine-unity",
"Coffee.UIParticle",
"Unity.Addressables",
"Unity.ResourceManager",
"Unity.TextMeshPro",
"UnityEngine.UI",
"Unity.InputSystem",
"Unity.VisualScripting.Core"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": true,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": false,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 4425bf94a69344e4c9d36f8c30a2a6ab
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,308 +0,0 @@
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
public class MissingScriptRemover : EditorWindow
{
private readonly List<Object> targetObjects = new List<Object>();
private readonly List<string> prefabPaths = new List<string>();
private bool includeSubFolders = true;
private bool includeScene = false;
private bool dryRun = true;
private bool lastRunWasScan = true;
private readonly Dictionary<string, int> scanResults = new Dictionary<string, int>();
private int totalMissing;
private int fileCount;
private Vector2 scrollPos;
private bool hasScanned = false;
[MenuItem("Tools/预制体丢失脚本清理工具")]
public static void ShowWindow()
{
GetWindow<MissingScriptRemover>("丢失脚本清理");
}
private void OnGUI()
{
EditorGUILayout.HelpBox(
"拖入 Prefab 或文件夹 → 扫描 → 清理\n" +
"扫描模式只统计不修改;确认无误后取消勾选「仅扫描」再点清理", MessageType.Info);
EditorGUILayout.Space(4);
EditorGUILayout.LabelField("目标(Prefab / 文件夹,可多选拖入)", EditorStyles.boldLabel);
Rect dropRect = GUILayoutUtility.GetRect(0, 40, GUILayout.ExpandWidth(true));
GUI.Box(dropRect, targetObjects.Count == 0
? "拖入 Prefab 或文件夹到此处"
: string.Join(", ", targetObjects.ConvertAll(o => o.name).ToArray()),
EditorStyles.helpBox);
Event evt = Event.current;
if ((evt.type == EventType.DragUpdated || evt.type == EventType.DragPerform) && dropRect.Contains(evt.mousePosition))
{
DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
if (evt.type == EventType.DragPerform)
{
DragAndDrop.AcceptDrag();
if (DragAndDrop.objectReferences.Length > 0)
{
targetObjects.Clear();
targetObjects.AddRange(DragAndDrop.objectReferences);
hasScanned = false;
}
}
evt.Use();
}
if (GUILayout.Button("使用 Project 当前选中", GUILayout.Height(22)))
{
Object[] sel = Selection.objects;
if (sel != null && sel.Length > 0)
{
targetObjects.Clear();
targetObjects.AddRange(sel);
hasScanned = false;
}
else
{
EditorUtility.DisplayDialog("提示", "请先在 Project 窗口选中 Prefab 或文件夹", "确定");
}
}
EditorGUILayout.Space(6);
includeSubFolders = EditorGUILayout.ToggleLeft("包含子文件夹", includeSubFolders);
includeScene = EditorGUILayout.ToggleLeft("同时处理当前打开的场景", includeScene);
dryRun = EditorGUILayout.ToggleLeft("仅扫描(不修改任何文件)", dryRun);
EditorGUILayout.Space(6);
EditorGUILayout.BeginHorizontal();
GUI.enabled = targetObjects.Count > 0 || includeScene;
if (GUILayout.Button("扫描", GUILayout.Height(28)))
{
ScanOrClean(true);
}
GUI.enabled = true;
GUI.enabled = (targetObjects.Count > 0 || includeScene) && !dryRun;
string cleanLabel = dryRun ? "清理(已禁用:勾选了仅扫描)" : "清理";
if (GUILayout.Button(cleanLabel, GUILayout.Height(28)))
{
if (EditorUtility.DisplayDialog("确认清理",
$"将永久删除丢失的脚本组件(不可撤销,建议先提交或备份)。\n目标: {(targetObjects.Count > 0 ? string.Join(", ", targetObjects.ConvertAll(o => o.name).ToArray()) : "")}{(includeScene ? " + 当前场景" : "")}\n\n确认继续?",
"确认清理", "取消"))
{
ScanOrClean(false);
}
}
GUI.enabled = true;
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(8);
if (hasScanned)
{
string modeText = lastRunWasScan ? "[扫描结果]" : "[清理结果]";
EditorGUILayout.LabelField($"{modeText} 共 {fileCount} 个文件,丢失脚本组件 {totalMissing} 处", EditorStyles.boldLabel);
scrollPos = EditorGUILayout.BeginScrollView(scrollPos);
foreach (var kv in scanResults)
{
if (kv.Value > 0)
{
Color old = GUI.color;
GUI.color = new Color(1f, 0.75f, 0.45f);
EditorGUILayout.LabelField($"{kv.Key} ({kv.Value})");
GUI.color = old;
}
}
EditorGUILayout.EndScrollView();
if (scanResults.Count > 0 && GUILayout.Button("定位选中第一个文件"))
{
foreach (var kv in scanResults)
{
if (kv.Value > 0 && kv.Key.EndsWith(".prefab"))
{
var obj = AssetDatabase.LoadMainAssetAtPath(kv.Key);
if (obj != null)
{
Selection.activeObject = obj;
EditorGUIUtility.PingObject(obj);
}
break;
}
}
}
}
}
private void ScanOrClean(bool scanOnly)
{
lastRunWasScan = scanOnly;
prefabPaths.Clear();
prefabPaths.AddRange(CollectPrefabPaths(targetObjects.ToArray()));
scanResults.Clear();
totalMissing = 0;
fileCount = 0;
int sceneMissing = 0;
bool cancelled = false;
try
{
for (int i = 0; i < prefabPaths.Count; i++)
{
string path = prefabPaths[i];
if (EditorUtility.DisplayCancelableProgressBar(
scanOnly ? "扫描丢失脚本" : "清理丢失脚本",
path, (float)i / prefabPaths.Count))
{
cancelled = true;
break;
}
int removed = ProcessPrefab(path, !scanOnly);
scanResults[path] = removed;
totalMissing += removed;
fileCount++;
}
if (includeScene)
{
sceneMissing = ProcessOpenScene(!scanOnly);
if (sceneMissing > 0)
{
scanResults["[当前场景]"] = sceneMissing;
totalMissing += sceneMissing;
}
fileCount++;
}
}
finally
{
EditorUtility.ClearProgressBar();
}
if (!scanOnly && totalMissing > 0)
{
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
hasScanned = true;
string summary = cancelled
? $"已取消。处理到第 {fileCount} 个,共发现 {totalMissing} 处丢失脚本"
: (scanOnly
? $"扫描完成: {fileCount} 个文件,共 {totalMissing} 处丢失脚本"
: $"清理完成: 处理 {fileCount} 个文件,删除 {totalMissing} 个丢失脚本组件");
if (scanOnly && totalMissing > 0)
summary += "\n取消勾选「仅扫描」后点击「清理」即可删除";
Debug.Log($"[MissingScriptRemover] {summary}");
EditorUtility.DisplayDialog(scanOnly ? "扫描完成" : "清理完成", summary, "确定");
}
private IEnumerable<string> CollectPrefabPaths(Object[] objects)
{
var result = new List<string>();
foreach (var obj in objects)
{
if (obj == null) continue;
string path = AssetDatabase.GetAssetPath(obj);
if (string.IsNullOrEmpty(path)) continue;
if (Directory.Exists(path))
{
if (includeSubFolders)
{
foreach (string guid in AssetDatabase.FindAssets("t:Prefab", new[] { path }))
{
string p = AssetDatabase.GUIDToAssetPath(guid);
if (!result.Contains(p)) result.Add(p);
}
}
else
{
foreach (string file in Directory.GetFiles(path, "*.prefab"))
{
string p = file.Replace('\\', '/');
if (!result.Contains(p)) result.Add(p);
}
}
}
else if (path.EndsWith(".prefab", System.StringComparison.OrdinalIgnoreCase))
{
if (!result.Contains(path)) result.Add(path);
}
}
result.Sort();
return result;
}
private int ProcessPrefab(string path, bool clean)
{
GameObject root = PrefabUtility.LoadPrefabContents(path);
int total = 0;
try
{
total = ProcessGameObjectTree(root, clean);
if (clean && total > 0)
{
PrefabUtility.SaveAsPrefabAsset(root, path);
}
}
finally
{
PrefabUtility.UnloadPrefabContents(root);
}
return total;
}
private int ProcessGameObjectTree(GameObject root, bool clean)
{
int total = 0;
total += HandleGameObject(root, clean);
Transform[] children = root.GetComponentsInChildren<Transform>(true);
foreach (Transform t in children)
{
if (t.gameObject == root) continue;
total += HandleGameObject(t.gameObject, clean);
}
return total;
}
private int HandleGameObject(GameObject go, bool clean)
{
if (clean)
{
return GameObjectUtility.RemoveMonoBehavioursWithMissingScript(go);
}
int count = 0;
foreach (Component c in go.GetComponents<Component>())
{
if (c == null) count++;
}
return count;
}
private int ProcessOpenScene(bool clean)
{
int total = 0;
var scene = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
foreach (GameObject rootObj in scene.GetRootGameObjects())
{
total += ProcessGameObjectTree(rootObj, clean);
}
if (clean && total > 0)
{
EditorSceneManager.MarkSceneDirty(scene);
}
return total;
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: e7d4b6c4f253fee4983cd2c93bf82849
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 2c9fe788ba8bf5a42985249734e8a908
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
File diff suppressed because it is too large Load Diff
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: aa94987a764257c41a7a321857b6f340
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,28 +0,0 @@
{
"name": "Game.Hotfix.Editor.Sprite",
"rootNamespace": "",
"references": [
"GameFramework",
"Game.Hotfix",
"CameraShake",
"spine-unity",
"Coffee.UIParticle",
"Unity.Addressables",
"Unity.ResourceManager",
"Unity.TextMeshPro",
"UnityEngine.UI",
"Unity.InputSystem",
"Unity.VisualScripting.Core"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": true,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": false,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 9ed81d17d74e7bc4d8acb3e10842ee35
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,886 +0,0 @@
using UnityEngine;
using UnityEditor;
using System.IO;
using System.Text;
using System.Collections.Generic;
using System.Linq;
/// <summary>
/// SpriteMetaRestorer Editor 窗口
///
/// 功能:将独立 .asset Sprite 文件的分割信息还原到 PNG.meta 的 spriteSheet.sprites 中。
/// 使用方法:
/// 1. 把 PNG 图片拖到"目标 PNG"槽位
/// 2. 把对应的 .asset Sprite 文件拖到列表区域(支持多选拖入)
/// 3. 点击"写入 Meta"完成还原
/// 4. 可选:勾选"写入后删除已融合的 .asset 文件"
///
/// 作者:AI Assistant
/// </summary>
public class SpriteMetaRestorer : EditorWindow
{
#region 字段
/// <summary>目标 PNG 纹理</summary>
private Texture2D targetTexture;
/// <summary>待处理的 Sprite .asset 文件列表</summary>
private List<SpriteAssetInfo> assetList = new List<SpriteAssetInfo>();
/// <summary>拖入区域的滚动位置</summary>
private Vector2 scrollPos;
/// <summary>处理结果日志</summary>
private string logText = "";
private Vector2 logScrollPos;
/// <summary>是否显示日志区域</summary>
private bool showLog = false;
/// <summary>写入完成后是否删除已融合的 .asset 文件及其 .meta</summary>
private bool deleteAssetsAfterWrite = true;
/// <summary>是否允许写入与 PNG 不匹配的 Sprite(textureGuid 不同)</summary>
private bool allowMismatchedSprites = false;
#endregion
#region 数据结构
/// <summary>单个 Sprite .asset 的解析信息</summary>
private class SpriteAssetInfo
{
/// <summary>.asset 文件路径(Assets/...)</summary>
public string assetPath;
/// <summary>Sprite 名称(m_Name)</summary>
public string name;
/// <summary>裁切矩形</summary>
public Rect rect;
/// <summary>轴心点</summary>
public Vector2 pivot;
/// <summary>边框</summary>
public Vector4 border;
/// <summary>spriteID(来自 .asset.meta 的 guid)</summary>
public string spriteID;
/// <summary>该 .asset 引用的纹理 guid,用于校验是否与目标 PNG 匹配</summary>
public string textureGuid;
/// <summary>解析是否成功</summary>
public bool valid;
public SpriteAssetInfo()
{
valid = false;
spriteID = "";
textureGuid = "";
rect = new Rect(0, 0, 0, 0);
pivot = new Vector2(0.5f, 0.5f);
border = new Vector4(0, 0, 0, 0);
}
}
#endregion
#region 菜单入口
[MenuItem("Tools/Sprite Meta Restorer")]
private static void ShowWindow()
{
// 获取已有窗口或创建新窗口
var window = GetWindow<SpriteMetaRestorer>("Sprite Meta Restorer");
window.minSize = new Vector2(420, 500);
window.Show();
}
#endregion
#region GUI 绘制
private void OnGUI()
{
DrawHeader();
DrawTargetPNGSlot();
DrawAssetDropZone();
DrawActionBar();
DrawLogArea();
}
/// <summary>
/// 顶部标题说明
/// </summary>
private void DrawHeader()
{
EditorGUILayout.Space(8);
EditorGUILayout.HelpBox(
"将 Sprite .asset 文件的分割信息还原到 PNG.meta 的 spriteSheet.sprites 中。\n" +
"1. 拖入目标 PNG 图片\n" +
"2. 拖入对应的 .asset 文件(支持多选)\n" +
"3. 点击「写入 Meta」",
MessageType.Info
);
EditorGUILayout.Space(4);
}
/// <summary>
/// 目标 PNG 拖拽槽位
/// </summary>
private void DrawTargetPNGSlot()
{
EditorGUILayout.LabelField("目标 PNG", EditorStyles.boldLabel);
// 使用 ObjectField 支持拖拽和点击选择
Rect dropRect = EditorGUILayout.GetControlRect(GUILayout.Height(60));
GUI.Box(dropRect, "", GUI.skin.box);
var centeredStyle = new GUIStyle(GUI.skin.label)
{
alignment = TextAnchor.MiddleCenter,
fontSize = 12
};
if (targetTexture != null)
{
// 显示已选择的 PNG 信息
string pngPath = AssetDatabase.GetAssetPath(targetTexture);
int texW = targetTexture.width;
int texH = targetTexture.height;
GUI.Label(dropRect, $"{Path.GetFileName(pngPath)}\n{texW} x {texH} px", centeredStyle);
}
else
{
GUI.Label(dropRect, "拖入 PNG 图片 或 点击选择", centeredStyle);
}
// 处理拖拽事件
if (Event.current.type == EventType.DragUpdated && dropRect.Contains(Event.current.mousePosition))
{
DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
Event.current.Use();
}
else if (Event.current.type == EventType.DragPerform && dropRect.Contains(Event.current.mousePosition))
{
DragAndDrop.AcceptDrag();
if (DragAndDrop.objectReferences.Length > 0)
{
var obj = DragAndDrop.objectReferences[0] as Texture2D;
if (obj != null)
{
string path = AssetDatabase.GetAssetPath(obj);
// 确保是 PNG 文件而非内部纹理
if (path.EndsWith(".png", System.StringComparison.OrdinalIgnoreCase))
{
targetTexture = obj;
assetList.Clear();
logText = "";
}
else
{
Debug.LogWarning("[SpriteMetaRestorer] 请拖入 PNG 文件,不是内部纹理");
}
}
}
Event.current.Use();
}
// 点击选择
targetTexture = (Texture2D)EditorGUI.ObjectField(
new Rect(dropRect.x, dropRect.y, dropRect.width, dropRect.height),
targetTexture, typeof(Texture2D), false
);
// 检查选择后是否为 PNG
if (targetTexture != null)
{
string p = AssetDatabase.GetAssetPath(targetTexture);
if (!p.EndsWith(".png", System.StringComparison.OrdinalIgnoreCase))
{
targetTexture = null;
}
}
EditorGUILayout.Space(8);
}
/// <summary>
/// .asset 文件拖拽列表区域
/// </summary>
private void DrawAssetDropZone()
{
EditorGUILayout.LabelField($"Sprite Assets ({assetList.Count} 个)", EditorStyles.boldLabel);
// 列表区域
Rect listRect = EditorGUILayout.GetControlRect(false, GUILayout.Height(200));
GUI.Box(listRect, "");
// 空状态提示
if (assetList.Count == 0 && targetTexture != null)
{
// 创建带灰色的 GUIStyle
var hintStyle = new GUIStyle(GUI.skin.label);
hintStyle.alignment = TextAnchor.MiddleCenter;
hintStyle.fontSize = 12;
hintStyle.normal.textColor = Color.gray;
GUI.Label(listRect, "拖入 .asset Sprite 文件到这里\n(支持 Project 窗口多选拖入)", hintStyle);
}
// 处理拖拽
if (Event.current.type == EventType.DragUpdated && listRect.Contains(Event.current.mousePosition))
{
DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
Event.current.Use();
}
else if (Event.current.type == EventType.DragPerform && listRect.Contains(Event.current.mousePosition))
{
DragAndDrop.AcceptDrag();
AddDraggedAssets(DragAndDrop.objectReferences);
Event.current.Use();
}
// 绘制列表(带滚动)
if (assetList.Count > 0)
{
var innerRect = new Rect(listRect.x + 4, listRect.y + 4, listRect.width - 8, listRect.height - 8);
scrollPos = GUI.BeginScrollView(innerRect, scrollPos, new Rect(0, 0, innerRect.width - 16, assetList.Count * 22));
for (int i = 0; i < assetList.Count; i++)
{
var info = assetList[i];
Rect rowRect = new Rect(0, i * 22, innerRect.width, 20);
// 背景色:有效=正常,无效=红底
Color origBg = GUI.backgroundColor;
GUI.backgroundColor = info.valid ? Color.green : Color.red;
// 名称
float nameW = rowRect.width * 0.35f;
GUI.Label(new Rect(rowRect.x, rowRect.y, nameW, 20), info.name);
// 坐标
float coordW = rowRect.width * 0.35f;
string coordText = $"({info.rect.x:F0},{info.rect.y:F0},{info.rect.width:F0}x{info.rect.height:F0})";
GUI.Label(new Rect(rowRect.x + nameW, rowRect.y, coordW, 20), coordText);
// 匹配状态
float statusW = rowRect.width * 0.15f;
if (targetTexture != null)
{
string pngGuid = GetAssetGuid(AssetDatabase.GetAssetPath(targetTexture) + ".meta");
bool matched = info.textureGuid == pngGuid;
// 正确方式:通过 GUIStyle.normal.textColor 设置颜色
var statusStyle = new GUIStyle(GUI.skin.label);
statusStyle.fontSize = 10;
statusStyle.normal.textColor = matched ? Color.green : Color.red;
GUI.Label(new Rect(rowRect.x + nameW + coordW, rowRect.y, statusW, 20),
matched ? "✓ 匹配" : "✗ 不匹配", statusStyle);
}
// 删除按钮
float btnW = rowRect.width * 0.12f;
if (GUI.Button(new Rect(rowRect.x + rowRect.width - btnW, rowRect.y, btnW, 20), "删除"))
{
assetList.RemoveAt(i);
i--;
}
GUI.backgroundColor = origBg;
}
GUI.EndScrollView();
}
EditorGUILayout.Space(6);
}
/// <summary>
/// 操作按钮区
/// </summary>
private void DrawActionBar()
{
EditorGUILayout.BeginHorizontal();
// 写入按钮
GUI.enabled = targetTexture != null && assetList.Count > 0;
if (GUILayout.Button("写入 Meta", GUILayout.Height(32)))
{
WriteSpritesToMeta();
}
GUI.enabled = true;
// 清空按钮
if (GUILayout.Button("清空列表", GUILayout.Height(32)))
{
assetList.Clear();
logText = "";
}
// 清空全部
if (GUILayout.Button("全部重置", GUILayout.Height(32)))
{
targetTexture = null;
assetList.Clear();
logText = "";
}
// 新增:删除所有与 PNG 不匹配的 Sprite
if (targetTexture != null && assetList.Count > 0)
{
if (GUILayout.Button("删除不匹配的 Sprite", GUILayout.Height(32)))
{
RemoveMismatchedSprites();
}
}
EditorGUILayout.EndHorizontal();
// 自动检测按钮
EditorGUILayout.Space(4);
if (GUILayout.Button("自动检测同目录 .asset 文件", GUILayout.Height(28)))
{
AutoDetectAssets();
}
// 删除 asset 选项
EditorGUILayout.Space(6);
deleteAssetsAfterWrite = EditorGUILayout.Toggle(
new GUIContent("写入后删除已融合的 .asset 文件",
"勾选后,写入 Meta 成功会自动删除列表中的 .asset 及其 .meta 文件"),
deleteAssetsAfterWrite
);
// 新增:是否允许写入与 PNG 不匹配的 Sprite
allowMismatchedSprites = EditorGUILayout.Toggle(
new GUIContent("允许写入与 PNG 不匹配的 Sprite",
"勾选:即使 Sprite 引用的纹理 guid 与当前 PNG 不同,也照样按其 rect 写入。\n不勾选:写入时会跳过这些不匹配的 Sprite。"),
allowMismatchedSprites
);
EditorGUILayout.Space(4);
}
/// <summary>
/// 日志区域
/// </summary>
private void DrawLogArea()
{
if (string.IsNullOrEmpty(logText)) return;
showLog = EditorGUILayout.Foldout(showLog, "处理日志");
if (showLog)
{
EditorGUILayout.BeginVertical(GUI.skin.box);
logScrollPos = EditorGUILayout.BeginScrollView(logScrollPos, GUILayout.Height(150));
EditorGUILayout.TextArea(logText, EditorStyles.wordWrappedLabel, GUILayout.ExpandHeight(true));
EditorGUILayout.EndScrollView();
EditorGUILayout.EndVertical();
}
}
#endregion
#region 拖拽处理
/// <summary>
/// 处理从 Project 窗口拖入的对象
/// </summary>
private void AddDraggedAssets(Object[] objects)
{
int added = 0;
int skipped = 0;
foreach (var obj in objects)
{
string path = AssetDatabase.GetAssetPath(obj);
// 只处理 .asset 文件
if (string.IsNullOrEmpty(path) ||
!path.EndsWith(".asset", System.StringComparison.OrdinalIgnoreCase))
{
skipped++;
continue;
}
// 检查是否已存在
if (assetList.Any(a => a.assetPath == path))
{
skipped++;
continue;
}
// 解析 .asset 文件
var info = ParseSpriteAsset(path);
if (info != null && info.valid)
{
assetList.Add(info);
added++;
}
else
{
Debug.LogWarning($"[SpriteMetaRestorer] 无法解析: {path}");
skipped++;
}
}
if (added > 0)
Debug.Log($"[SpriteMetaRestorer] 添加 {added} 个 .asset,跳过 {skipped} 个");
}
#endregion
#region 自动检测
/// <summary>
/// 自动检测目标 PNG 同目录下的所有 Sprite 类型 .asset 文件
/// </summary>
private void AutoDetectAssets()
{
if (targetTexture == null)
{
EditorUtility.DisplayDialog("提示", "请先选择目标 PNG", "确定");
return;
}
string pngPath = AssetDatabase.GetAssetPath(targetTexture);
string pngDir = Path.GetDirectoryName(pngPath).Replace("\\", "/");
string pngGuid = GetAssetGuid(pngPath + ".meta");
if (string.IsNullOrEmpty(pngGuid))
{
EditorUtility.DisplayDialog("错误", "无法获取 PNG 的 guid", "确定");
return;
}
// 查找同目录下所有 .asset 文件
string[] assetFiles = Directory.GetFiles(pngDir, "*.asset");
int added = 0;
foreach (string file in assetFiles)
{
string assetPath = file.Replace("\\", "/");
// 跳过已存在的
if (assetList.Any(a => a.assetPath == assetPath))
continue;
// 解析并检查是否为 Sprite 类型且匹配当前 PNG
var info = ParseSpriteAsset(assetPath);
if (info != null && info.valid)
{
// 检查 textureGuid 是否匹配
if (info.textureGuid == pngGuid)
{
assetList.Add(info);
added++;
}
}
}
Debug.Log($"[SpriteMetaRestorer] 自动检测到 {added} 个匹配的 .asset 文件");
Repaint();
}
#endregion
#region .asset 解析
/// <summary>
/// 解析单个 Sprite .asset 文件,提取名称、rect、pivot、border、textureGuid
/// </summary>
private SpriteAssetInfo ParseSpriteAsset(string assetPath)
{
string fullPath = Path.GetFullPath(assetPath);
if (!File.Exists(fullPath))
return null;
string text = File.ReadAllText(fullPath, Encoding.UTF8);
// 检查是否是 Sprite 类型(!u!213),而非 MonoBehaviour(!u!114)
// Split by --- and check second document
string[] docs = text.Split(new[] { "---" }, System.StringSplitOptions.None);
if (docs.Length < 2 || !docs[1].Contains("!u!213"))
return null;
var info = new SpriteAssetInfo();
info.assetPath = assetPath;
// 提取 m_Name
info.name = ExtractField(text, "m_Name");
if (string.IsNullOrEmpty(info.name))
info.name = Path.GetFileNameWithoutExtension(assetPath);
// 提取 m_Rect
info.rect = ExtractRect(text);
// 提取 m_Pivot
info.pivot = ExtractVec2(text, "m_Pivot");
// 提取 m_Border
info.border = ExtractVec4(text, "m_Border");
// 提取 texture guid
info.textureGuid = ExtractTextureGuid(text);
// 提取 .asset.meta 的 guid 作为 spriteID
string metaPath = assetPath + ".meta";
if (File.Exists(metaPath))
{
string metaText = File.ReadAllText(metaPath, Encoding.UTF8);
info.spriteID = GetGuidFromText(metaText);
}
// 基本校验
info.valid = info.rect.width > 0 && info.rect.height > 0;
return info;
}
/// <summary>提取 m_Name 字段值</summary>
private string ExtractField(string text, string fieldName)
{
int idx = text.IndexOf(fieldName + ":");
if (idx < 0) return "";
int start = idx + fieldName.Length;
// 跳过冒号后的空格
while (start < text.Length && (text[start] == ':' || text[start] == ' ' || text[start] == '\t'))
start++;
int end = start;
while (end < text.Length && text[end] != '\n' && text[end] != '\r')
end++;
return text.Substring(start, end - start).Trim();
}
/// <summary>提取 m_Rect 块</summary>
private Rect ExtractRect(string text)
{
string marker = "m_Rect:";
int idx = text.IndexOf(marker);
if (idx < 0) return new Rect(0, 0, 0, 0);
// 找到 marker 之后的 x/y/width/height
string block = text.Substring(idx);
float x = ExtractFloatAfter(block, "x:");
float y = ExtractFloatAfter(block, "y:");
float w = ExtractFloatAfter(block, "width:");
float h = ExtractFloatAfter(block, "height:");
return new Rect(x, y, w, h);
}
/// <summary>提取 m_Pivot 二维向量</summary>
private Vector2 ExtractVec2(string text, string fieldName)
{
string marker = fieldName + ":";
int idx = text.IndexOf(marker);
if (idx < 0) return new Vector2(0.5f, 0.5f);
string block = text.Substring(idx);
float x = ExtractFloatAfter(block, "x:");
float y = ExtractFloatAfter(block, "y:");
return new Vector2(x, y);
}
/// <summary>提取 m_Border 四维向量</summary>
private Vector4 ExtractVec4(string text, string fieldName)
{
string marker = fieldName + ":";
int idx = text.IndexOf(marker);
if (idx < 0) return new Vector4(0, 0, 0, 0);
string block = text.Substring(idx);
float x = ExtractFloatAfter(block, "x:");
float y = ExtractFloatAfter(block, "y:");
float z = ExtractFloatAfter(block, "z:");
float w = ExtractFloatAfter(block, "w:");
return new Vector4(x, y, z, w);
}
/// <summary>提取 m_RD.texture 中的 guid</summary>
private string ExtractTextureGuid(string text)
{
int idx = text.IndexOf("texture:");
if (idx < 0) return "";
string block = text.Substring(idx);
int guidIdx = block.IndexOf("guid:");
if (guidIdx < 0) return "";
int start = guidIdx + 5;
int end = start;
while (end < block.Length && block[end] != ',' && block[end] != '}' && block[end] != '\n')
end++;
return block.Substring(start, end - start).Trim();
}
/// <summary>在文本中提取指定字段后的浮点数</summary>
private float ExtractFloatAfter(string text, string fieldName)
{
int idx = text.IndexOf(fieldName);
if (idx < 0) return 0f;
int start = idx + fieldName.Length;
// 跳过到数字
while (start < text.Length && !char.IsDigit(text[start]) && text[start] != '-' && text[start] != '.')
start++;
int end = start;
while (end < text.Length && (char.IsDigit(text[end]) || text[end] == '.' || text[end] == '-'
|| text[end] == 'e' || text[end] == 'E' || text[end] == '+'))
end++;
float result;
if (float.TryParse(text.Substring(start, end - start), System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture, out result))
return result;
return 0f;
}
#endregion
#region Meta 文件操作
/// <summary>
/// 删除所有与当前 PNG 不匹配的 Sprite(textureGuid 不同)
/// </summary>
private void RemoveMismatchedSprites()
{
if (targetTexture == null || assetList.Count == 0)
return;
string pngGuid = GetAssetGuid(AssetDatabase.GetAssetPath(targetTexture) + ".meta");
if (string.IsNullOrEmpty(pngGuid))
return;
int before = assetList.Count;
assetList.RemoveAll(a => a.textureGuid != pngGuid);
int removed = before - assetList.Count;
if (removed > 0)
{
Debug.Log($"[SpriteMetaRestorer] 已删除 {removed} 个与目标 PNG 不匹配的 Sprite");
Repaint();
}
}
/// <summary>
/// 将解析到的 sprite 列表写入 .png.meta 的 spriteSheet.sprites 块
/// </summary>
private void WriteSpritesToMeta()
{
if (targetTexture == null || assetList.Count == 0)
return;
string pngPath = AssetDatabase.GetAssetPath(targetTexture);
string metaPath = pngPath + ".meta";
if (!File.Exists(metaPath))
{
EditorUtility.DisplayDialog("错误", $"找不到 meta 文件:\n{metaPath}", "确定");
return;
}
string metaText = File.ReadAllText(metaPath, Encoding.UTF8);
StringBuilder sb = new StringBuilder();
string pngGuid = GetAssetGuid(metaPath);
// 构建 sprites YAML 文本
StringBuilder spritesYaml = new StringBuilder();
List<string> nameFileIdEntries = new List<string>();
foreach (var info in assetList)
{
if (!info.valid)
continue;
// ★★★ 新增:不匹配 PNG 时的处理逻辑 ★★★
bool matched = info.textureGuid == pngGuid;
if (!matched && !allowMismatchedSprites)
{
sb.AppendLine($" [跳过] {info.name}(textureGuid 不匹配当前 PNG)");
continue;
}
// ★★★ 原有写入逻辑保持不变 ★★★
spritesYaml.AppendLine(" - serializedVersion: 2");
spritesYaml.AppendLine($" name: {info.name}");
spritesYaml.AppendLine(" rect:");
spritesYaml.AppendLine(" serializedVersion: 2");
spritesYaml.AppendLine($" x: {FormatFloat(info.rect.x)}");
spritesYaml.AppendLine($" y: {FormatFloat(info.rect.y)}");
spritesYaml.AppendLine($" width: {FormatFloat(info.rect.width)}");
spritesYaml.AppendLine($" height: {FormatFloat(info.rect.height)}");
spritesYaml.AppendLine(" alignment: 0");
spritesYaml.AppendLine($" pivot: {{x: {FormatFloat(info.pivot.x)}, y: {FormatFloat(info.pivot.y)}}}");
spritesYaml.AppendLine($" border: {{x: {FormatFloat(info.border.x)}, y: {FormatFloat(info.border.y)}, z: {FormatFloat(info.border.z)}, w: {FormatFloat(info.border.w)}}}");
spritesYaml.AppendLine(" outline: []");
spritesYaml.AppendLine(" physicsShape: []");
spritesYaml.AppendLine(" tessellationDetail: -1");
spritesYaml.AppendLine(" bones: []");
spritesYaml.AppendLine($" spriteID: {info.spriteID}");
spritesYaml.AppendLine(" internalID: 0");
spritesYaml.AppendLine(" vertices: []");
spritesYaml.AppendLine(" indices: ");
spritesYaml.AppendLine(" edges: []");
spritesYaml.AppendLine(" weights: []");
nameFileIdEntries.Add($" {info.name}: 0");
sb.AppendLine($" [+] {info.name} rect=({info.rect.x:F0},{info.rect.y:F0},{info.rect.width:F0}x{info.rect.height:F0})");
}
string spritesBlock = spritesYaml.ToString().TrimEnd('\n', '\r');
string spritesContent = " sprites:\n" + spritesBlock + "\n";
// 替换 meta 中的 sprites 块(保持你原来的逻辑)
string result;
if (metaText.Contains("sprites: []"))
{
result = ReplaceFirst(metaText, " sprites: []", spritesContent);
}
else
{
int idx = metaText.IndexOf(" sprites:");
if (idx >= 0)
{
int endIdx = FindBlockEnd(metaText, idx);
result = metaText.Substring(0, idx) + spritesContent + metaText.Substring(endIdx);
}
else
{
result = metaText;
}
}
// 更新 nameFileIdTable(保持你原来的逻辑)
if (nameFileIdEntries.Count > 0)
{
string tableBlock = " nameFileIdTable:\n" + string.Join("\n", nameFileIdEntries) + "\n";
int tblIdx = result.IndexOf(" nameFileIdTable:");
if (tblIdx >= 0)
{
int tblEndIdx = FindBlockEnd(result, tblIdx);
result = result.Substring(0, tblIdx) + tableBlock + result.Substring(tblEndIdx);
}
}
File.WriteAllText(metaPath, result, Encoding.UTF8);
AssetDatabase.ImportAsset(pngPath);
int validCount = assetList.Count(a => a.valid);
sb.AppendLine($"\n[完成] 已写入 {validCount} 个 Sprite 到:");
sb.AppendLine($" {metaPath}");
sb.AppendLine("\n→ Unity 已自动 Reimport,请在 Inspector 中确认。");
logText = sb.ToString();
showLog = true;
}
/// <summary>
/// 替换字符串中第一次出现的 oldText
/// </summary>
private string ReplaceFirst(string text, string oldText, string newText)
{
int idx = text.IndexOf(oldText);
if (idx < 0) return text;
return text.Substring(0, idx) + newText + text.Substring(idx + oldText.Length);
}
/// <summary>
/// 从 blockStartIndex 开始,找到当前 YAML 块的结束位置。
/// 块以 " keyName:" 开头,后续行要么以 " - " 继续列表项,
/// 要么以更多缩进(6+ 空格或 " ")作为子属性。
/// 遇到同级缩进(4空格+非"-"非空格)的行时结束。
/// </summary>
private int FindBlockEnd(string text, int blockStartIndex)
{
int i = blockStartIndex;
// 跳过 " sprites:" 这一行
int lineEnd = text.IndexOf('\n', i);
if (lineEnd < 0) return text.Length;
i = lineEnd + 1;
while (i < text.Length)
{
// 找到当前行开头
int nextNewline = text.IndexOf('\n', i);
if (nextNewline < 0) nextNewline = text.Length;
string line = text.Substring(i, nextNewline - i);
// 空行不结束块(跳过)
if (line.Trim().Length == 0)
{
i = nextNewline + 1;
continue;
}
// 以 " - " 开头 → 仍是列表项,继续
if (line.StartsWith(" - "))
{
i = nextNewline + 1;
continue;
}
// 以 " "(6空格)开头 → 子属性,继续
if (line.StartsWith(" "))
{
i = nextNewline + 1;
continue;
}
// 以 " "(4空格)开头但不是 "- " → 下一个同级字段,块结束
if (line.StartsWith(" ") && !line.StartsWith(" - ") && !line.StartsWith(" "))
{
return i;
}
// 其他情况(缩进更少),块结束
return i;
}
return text.Length;
}
/// <summary>
/// 格式化浮点数:整数值不带小数点
/// </summary>
private string FormatFloat(float v)
{
if (v == Mathf.RoundToInt(v))
return Mathf.RoundToInt(v).ToString();
return v.ToString("G7", System.Globalization.CultureInfo.InvariantCulture);
}
#endregion
#region 工具方法
/// <summary>
/// 从 meta 文本提取 guid
/// </summary>
private string GetAssetGuid(string metaPath)
{
if (!File.Exists(metaPath)) return "";
string text = File.ReadAllText(metaPath, Encoding.UTF8);
return GetGuidFromText(text);
}
private string GetGuidFromText(string text)
{
int idx = text.IndexOf("guid:");
if (idx < 0) return "";
int start = idx + 5;
int end = start;
while (end < text.Length && text[end] != '\n' && text[end] != '\r')
end++;
return text.Substring(start, end - start).Trim();
}
#endregion
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 60f9d9a0a7814054a8ead7eed4a096f1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 0a72f4e051c99184c97f1dd33f12da5a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,375 +0,0 @@
// Assets/Editor/BulkTextTool.cs
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
#if TMP_PRESENT
using TMPro;
#endif
public class BulkTextTool : EditorWindow
{
// 扫描范围
private List<string> targetFolders = new List<string>(); // 空 = 全项目
private bool includeScenes = true;
private bool includePrefabs = true;
// UGUI Text 选项
private bool applyUiText = true;
private bool uiSetFont;
private Font uiFont;
private bool uiSetFontSize;
private int uiFontSize = 24;
private bool uiSetColor;
private Color uiColor = Color.white;
private bool uiSetRichText;
private bool uiRichText = true;
private bool uiSetRaycast;
private bool uiRaycast = false;
private bool uiSetAlignment;
private TextAnchor uiAlignment = TextAnchor.MiddleCenter;
private bool uiSetHorizontalOverflow;
private HorizontalWrapMode uiHWrap = HorizontalWrapMode.Wrap;
private bool uiSetVerticalOverflow;
private VerticalWrapMode uiVWrap = VerticalWrapMode.Truncate;
// TMP 选项
#if TMP_PRESENT
private bool applyTmpText = true;
private bool tmpSetFontAsset;
private TMP_FontAsset tmpFontAsset;
private bool tmpSetFontSize;
private float tmpFontSize = 24f;
private bool tmpSetColor;
private Color tmpColor = Color.white;
private bool tmpSetRaycast;
private bool tmpRaycast = false;
private bool tmpSetAlignment;
private TextAlignmentOptions tmpAlignment = TextAlignmentOptions.Center;
private bool tmpSetWrapping;
private bool tmpEnableWordWrapping = true;
private bool tmpSetOverflow;
private TextOverflowModes tmpOverflow = TextOverflowModes.Overflow;
#else
private const bool applyTmpText = false; // 未检测到 TMP
#endif
// 执行控制
private bool dryRun = false;
// 报表
private int totalAssetsProcessed;
private int totalComponentsChanged;
private int totalErrors;
[MenuItem("Tools/Bulk Text Tool")]
public static void Open() => GetWindow<BulkTextTool>("Bulk Text Tool");
private void OnGUI()
{
EditorGUILayout.LabelField("扫描范围", EditorStyles.boldLabel);
includeScenes = EditorGUILayout.Toggle(new GUIContent("包含场景 (.unity)"), includeScenes);
includePrefabs = EditorGUILayout.Toggle(new GUIContent("包含预制体 (.prefab)"), includePrefabs);
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("Use Selected Folders"))
{
targetFolders = Selection.GetFiltered<Object>(SelectionMode.Assets)
.Select(AssetDatabase.GetAssetPath)
.Select(path => AssetDatabase.IsValidFolder(path) ? path : Path.GetDirectoryName(path).Replace("\\", "/"))
.Where(p => !string.IsNullOrEmpty(p))
.Distinct()
.ToList();
}
if (GUILayout.Button("Clear Folders")) targetFolders.Clear();
}
EditorGUILayout.HelpBox(
targetFolders.Count == 0 ? "范围:全项目" : $"范围:{string.Join(", ", targetFolders)}",
MessageType.Info);
EditorGUILayout.Space();
DrawUiTextSection();
EditorGUILayout.Space();
DrawTmpTextSection();
EditorGUILayout.Space();
dryRun = EditorGUILayout.Toggle(new GUIContent("Dry run(仅预览,不保存)"), dryRun);
EditorGUILayout.Space();
using (new EditorGUILayout.HorizontalScope())
{
GUI.enabled = includeScenes || includePrefabs;
if (GUILayout.Button("Run(场景+预制体)")) RunBatch(includeScenes, includePrefabs);
GUI.enabled = includeScenes;
if (GUILayout.Button("Run(仅场景)")) RunBatch(true, false);
GUI.enabled = includePrefabs;
if (GUILayout.Button("Run(仅预制体)")) RunBatch(false, true);
GUI.enabled = true;
}
EditorGUILayout.Space();
EditorGUILayout.LabelField("报表", EditorStyles.boldLabel);
EditorGUILayout.LabelField($"处理资产数:{totalAssetsProcessed}");
EditorGUILayout.LabelField($"改动组件数:{totalComponentsChanged}");
EditorGUILayout.LabelField($"错误数:{totalErrors}");
}
private void DrawUiTextSection()
{
EditorGUILayout.LabelField("UGUI Text", EditorStyles.boldLabel);
applyUiText = EditorGUILayout.Toggle(new GUIContent("应用到 UnityEngine.UI.Text"), applyUiText);
using (new EditorGUI.DisabledScope(!applyUiText))
{
uiSetFont = EditorGUILayout.Toggle("修改 Font", uiSetFont);
if (uiSetFont) uiFont = (Font)EditorGUILayout.ObjectField("Font", uiFont, typeof(Font), false);
uiSetFontSize = EditorGUILayout.Toggle("修改 Font Size", uiSetFontSize);
if (uiSetFontSize) uiFontSize = EditorGUILayout.IntField("Font Size", uiFontSize);
uiSetColor = EditorGUILayout.Toggle("修改 Color", uiSetColor);
if (uiSetColor) uiColor = EditorGUILayout.ColorField("Color", uiColor);
uiSetRichText = EditorGUILayout.Toggle("修改 Rich Text", uiSetRichText);
if (uiSetRichText) uiRichText = EditorGUILayout.Toggle("Rich Text", uiRichText);
uiSetRaycast = EditorGUILayout.Toggle("修改 Raycast Target", uiSetRaycast);
if (uiSetRaycast) uiRaycast = EditorGUILayout.Toggle("Raycast Target", uiRaycast);
uiSetAlignment = EditorGUILayout.Toggle("修改 Alignment", uiSetAlignment);
if (uiSetAlignment) uiAlignment = (TextAnchor)EditorGUILayout.EnumPopup("Alignment", uiAlignment);
uiSetHorizontalOverflow = EditorGUILayout.Toggle("修改 Horizontal Wrap", uiSetHorizontalOverflow);
if (uiSetHorizontalOverflow) uiHWrap = (HorizontalWrapMode)EditorGUILayout.EnumPopup("Horizontal Wrap", uiHWrap);
uiSetVerticalOverflow = EditorGUILayout.Toggle("修改 Vertical Overflow", uiSetVerticalOverflow);
if (uiSetVerticalOverflow) uiVWrap = (VerticalWrapMode)EditorGUILayout.EnumPopup("Vertical Overflow", uiVWrap);
}
}
private void DrawTmpTextSection()
{
#if TMP_PRESENT
EditorGUILayout.LabelField("TextMeshPro (TMP_Text)", EditorStyles.boldLabel);
applyTmpText = EditorGUILayout.Toggle(new GUIContent("应用到 TMPro.TMP_Text"), applyTmpText);
using (new EditorGUI.DisabledScope(!applyTmpText))
{
tmpSetFontAsset = EditorGUILayout.Toggle("修改 TMP Font Asset", tmpSetFontAsset);
if (tmpSetFontAsset) tmpFontAsset = (TMP_FontAsset)EditorGUILayout.ObjectField("TMP Font Asset", tmpFontAsset, typeof(TMP_FontAsset), false);
tmpSetFontSize = EditorGUILayout.Toggle("修改 Font Size", tmpSetFontSize);
if (tmpSetFontSize) tmpFontSize = EditorGUILayout.FloatField("Font Size", tmpFontSize);
tmpSetColor = EditorGUILayout.Toggle("修改 Color", tmpSetColor);
if (tmpSetColor) tmpColor = EditorGUILayout.ColorField("Color", tmpColor);
tmpSetRaycast = EditorGUILayout.Toggle("修改 Raycast Target", tmpSetRaycast);
if (tmpSetRaycast) tmpRaycast = EditorGUILayout.Toggle("Raycast Target", tmpRaycast);
tmpSetAlignment = EditorGUILayout.Toggle("修改 Alignment", tmpSetAlignment);
if (tmpSetAlignment) tmpAlignment = (TextAlignmentOptions)EditorGUILayout.EnumPopup("Alignment", tmpAlignment);
tmpSetWrapping = EditorGUILayout.Toggle("修改 Word Wrapping", tmpSetWrapping);
if (tmpSetWrapping) tmpEnableWordWrapping = EditorGUILayout.Toggle("Enable Word Wrapping", tmpEnableWordWrapping);
tmpSetOverflow = EditorGUILayout.Toggle("修改 Overflow Mode", tmpSetOverflow);
if (tmpSetOverflow) tmpOverflow = (TextOverflowModes)EditorGUILayout.EnumPopup("Overflow Mode", tmpOverflow);
}
#else
EditorGUILayout.LabelField("TextMeshPro (TMP_Text)", EditorStyles.boldLabel);
EditorGUILayout.HelpBox("未检测到 TMP。导入 TextMeshPro 后,在 Scripting Define Symbols 定义 TMP_PRESENT 以启用 TMP 设置。", MessageType.Info);
#endif
}
private void RunBatch(bool processScenes, bool processPrefabs)
{
totalAssetsProcessed = 0;
totalComponentsChanged = 0;
totalErrors = 0;
var guids = CollectAssetGuids(processScenes, processPrefabs);
try
{
AssetDatabase.StartAssetEditing();
foreach (var guid in guids)
{
var path = AssetDatabase.GUIDToAssetPath(guid);
bool changed = false;
if (path.EndsWith(".unity"))
{
changed = ProcessScene(path);
}
else if (path.EndsWith(".prefab"))
{
changed = ProcessPrefab(path);
}
totalAssetsProcessed++;
if (changed && !dryRun)
{
AssetDatabase.SaveAssets();
}
}
}
finally
{
AssetDatabase.StopAssetEditing();
AssetDatabase.Refresh();
}
EditorUtility.DisplayDialog("Bulk Text Tool",
$"完成。\n处理资产数:{totalAssetsProcessed}\n改动组件数:{totalComponentsChanged}\n错误数:{totalErrors}",
"OK");
}
private IEnumerable<string> CollectAssetGuids(bool scenes, bool prefabs)
{
var filters = new List<string>();
if (scenes) filters.Add("t:scene");
if (prefabs) filters.Add("t:prefab");
var scope = targetFolders.Count == 0 ? null : targetFolders.ToArray();
var set = new HashSet<string>();
foreach (var f in filters)
{
var guids = scope == null ? AssetDatabase.FindAssets(f) : AssetDatabase.FindAssets(f, scope);
foreach (var g in guids) set.Add(g);
}
return set;
}
private bool ProcessScene(string path)
{
bool changedAny = false;
var scene = EditorSceneManager.OpenScene(path, OpenSceneMode.Single);
try
{
foreach (var root in scene.GetRootGameObjects())
{
changedAny |= ProcessHierarchy(root);
}
if (changedAny && !dryRun)
{
EditorSceneManager.MarkSceneDirty(scene);
EditorSceneManager.SaveScene(scene);
}
}
catch (System.Exception ex)
{
totalErrors++;
Debug.LogError($"[BulkTextTool] 场景处理错误:{path}\n{ex}");
}
return changedAny;
}
private bool ProcessPrefab(string path)
{
bool changedAny = false;
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
if (prefab == null) return false;
var instance = (GameObject)PrefabUtility.InstantiatePrefab(prefab);
try
{
changedAny = ProcessHierarchy(instance);
if (changedAny && !dryRun)
{
PrefabUtility.ApplyPrefabInstance(instance, InteractionMode.AutomatedAction);
}
}
catch (System.Exception ex)
{
totalErrors++;
Debug.LogError($"[BulkTextTool] 预制体处理错误:{path}\n{ex}");
}
finally
{
if (instance != null) DestroyImmediate(instance);
}
return changedAny;
}
private bool ProcessHierarchy(GameObject root)
{
bool changed = false;
// UGUI Text
if (applyUiText)
{
var texts = root.GetComponentsInChildren<Text>(true);
foreach (var t in texts)
{
if (ApplyUiTextChanges(t))
{
totalComponentsChanged++;
changed = true;
if (!dryRun) EditorUtility.SetDirty(t);
}
}
}
// TMP Text
#if TMP_PRESENT
if (applyTmpText)
{
var tmps = root.GetComponentsInChildren<TMP_Text>(true);
foreach (var tt in tmps)
{
if (ApplyTmpTextChanges(tt))
{
totalComponentsChanged++;
changed = true;
if (!dryRun) EditorUtility.SetDirty(tt);
}
}
}
#endif
return changed;
}
private bool ApplyUiTextChanges(Text t)
{
bool changed = false;
if (uiSetFont && t.font != uiFont) { t.font = uiFont; changed = true; }
if (uiSetFontSize && t.fontSize != uiFontSize) { t.fontSize = uiFontSize; changed = true; }
if (uiSetColor && t.color != uiColor) { t.color = uiColor; changed = true; }
if (uiSetRichText && t.supportRichText != uiRichText) { t.supportRichText = uiRichText; changed = true; }
if (uiSetRaycast && t.raycastTarget != uiRaycast) { t.raycastTarget = uiRaycast; changed = true; }
if (uiSetAlignment && t.alignment != uiAlignment) { t.alignment = uiAlignment; changed = true; }
if (uiSetHorizontalOverflow && t.horizontalOverflow != uiHWrap) { t.horizontalOverflow = uiHWrap; changed = true; }
if (uiSetVerticalOverflow && t.verticalOverflow != uiVWrap) { t.verticalOverflow = uiVWrap; changed = true; }
return changed;
}
#if TMP_PRESENT
private bool ApplyTmpTextChanges(TMP_Text tt)
{
bool changed = false;
if (tmpSetFontAsset && tt.font != tmpFontAsset) { tt.font = tmpFontAsset; changed = true; }
if (tmpSetFontSize && Mathf.Abs(tt.fontSize - tmpFontSize) > 0.001f) { tt.fontSize = tmpFontSize; changed = true; }
if (tmpSetColor && tt.color != tmpColor) { tt.color = tmpColor; changed = true; }
if (tmpSetRaycast && tt.raycastTarget != tmpRaycast) { tt.raycastTarget = tmpRaycast; changed = true; }
if (tmpSetAlignment && tt.alignment != tmpAlignment) { tt.alignment = tmpAlignment; changed = true; }
if (tmpSetWrapping && tt.enableWordWrapping != tmpEnableWordWrapping) { tt.enableWordWrapping = tmpEnableWordWrapping; changed = true; }
if (tmpSetOverflow && tt.overflowMode != tmpOverflow) { tt.overflowMode = tmpOverflow; changed = true; }
return changed;
}
#endif
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 0333c7f37706ce14abd6cec5cc2a05e3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,28 +0,0 @@
{
"name": "Game.Hotfix.Editor.TextMeshPro",
"rootNamespace": "",
"references": [
"GameFramework",
"Game.Hotfix",
"CameraShake",
"spine-unity",
"Coffee.UIParticle",
"Unity.Addressables",
"Unity.ResourceManager",
"Unity.TextMeshPro",
"UnityEngine.UI",
"Unity.InputSystem",
"Unity.VisualScripting.Core"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": true,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": false,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: c6c1bb22a635c20488447400fd4b9517
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,54 +0,0 @@
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using TMPro;
using System.IO;
using System.Collections.Generic;
public class CollectPlayerNameChars : EditorWindow
{
[MenuItem("Tools/收集玩家名字字符(场景+预制体)")]
static void Collect()
{
HashSet<char> chars = new HashSet<char>();
// 1. 基础字符集
string baseChars = "一二三四五六七八九十" +
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" +
"0123456789" +
"_-.,";
foreach (char c in baseChars) chars.Add(c);
// 2. 扫描所有场景
foreach (var scene in EditorBuildSettings.scenes)
{
if (!scene.enabled) continue;
var sceneObj = EditorSceneManager.OpenScene(scene.path);
foreach (var tmpText in Object.FindObjectsOfType<TMP_Text>())
{
foreach (char c in tmpText.text) chars.Add(c);
}
}
// 3. 扫描所有预制体
string[] prefabGuids = AssetDatabase.FindAssets("t:Prefab");
foreach (string guid in prefabGuids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
if (prefab == null) continue;
foreach (var tmpText in prefab.GetComponentsInChildren<TMP_Text>(true))
{
foreach (char c in tmpText.text) chars.Add(c);
}
}
// 4. 保存到文本文件
string savePath = Application.dataPath + "/player_name_chars.txt";
File.WriteAllText(savePath, new string(new List<char>(chars).ToArray()));
AssetDatabase.Refresh();
Debug.Log($"已收集 {chars.Count} 个字符,保存至:{savePath}");
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: d67c9aedd067b4a4ea3353883e2764cb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: de053aa227f6cf74299f5972d39d4114
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,15 +0,0 @@
using UnityEditor;
using UnityEngine;
public class AtlasImporter : AssetPostprocessor
{
void OnPreprocessAsset()
{
if (assetPath.EndsWith(".atlas"))
{
var text = System.IO.File.ReadAllText(assetPath);
var textAsset = new TextAsset(text);
AssetDatabase.CreateAsset(textAsset, assetPath + ".asset");
}
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: f23f5970c41db1042819b020909cdcdc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,28 +0,0 @@
{
"name": "Game.Hotfix.Editor.UITool",
"rootNamespace": "",
"references": [
"GameFramework",
"Game.Hotfix",
"CameraShake",
"spine-unity",
"Coffee.UIParticle",
"Unity.Addressables",
"Unity.ResourceManager",
"Unity.TextMeshPro",
"UnityEngine.UI",
"Unity.InputSystem",
"Unity.VisualScripting.Core"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": true,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": false,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 9a5acb94ee3f63e439c36bd833f837ba
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,940 +0,0 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using UnityEditor;
using UnityEngine;
using UnityEngine.UI;
#region 单线程
public class PsbToUGUIWindow : EditorWindow
{
[Header("Inputs")]
private GameObject sourceRoot;
private Canvas targetCanvas;
[Header("Config")]
private float referencePPU = 100f; // Match PSD Importer Pixels Per Unit
private bool copyAlphaFromRenderer = true;
private bool preserveSortingOrder = true;
private bool addRaycastTargets = false; // For UI images; off if purely visual
private bool autoCreateCanvasIfMissing = true;
private string parentName = "PSB_UI";
// 新增 Toggle
private bool exportAsPng = false;
[MenuItem("Tools/PSB → UGUI Converter")]
public static void ShowWindow()
{
var win = GetWindow<PsbToUGUIWindow>("PSB → UGUI");
win.minSize = new Vector2(420, 360);
}
private void OnGUI()
{
EditorGUILayout.LabelField("Source", EditorStyles.boldLabel);
sourceRoot = (GameObject)EditorGUILayout.ObjectField(
new GUIContent("PSB root (scene GameObject)", "Select the instantiated PSB root that contains SpriteRenderers"),
sourceRoot, typeof(GameObject), true);
EditorGUILayout.Space();
EditorGUILayout.LabelField("Target", EditorStyles.boldLabel);
targetCanvas = (Canvas)EditorGUILayout.ObjectField(
new GUIContent("Target Canvas", "Canvas to place generated UI under"),
targetCanvas, typeof(Canvas), true);
autoCreateCanvasIfMissing = EditorGUILayout.Toggle(
new GUIContent("Auto-create canvas if missing", "Create an Overlay Canvas+EventSystem when none provided"),
autoCreateCanvasIfMissing
);
EditorGUILayout.Space();
EditorGUILayout.LabelField("Conversion settings", EditorStyles.boldLabel);
referencePPU = EditorGUILayout.FloatField(
new GUIContent("Reference Pixels Per Unit", "Use the PSD Importer PPU (often 100). Controls world→UI pixel mapping"),
referencePPU
);
copyAlphaFromRenderer = EditorGUILayout.Toggle(
new GUIContent("Copy alpha from SpriteRenderer color", "Image color.a = SpriteRenderer.color.a"),
copyAlphaFromRenderer
);
addRaycastTargets = EditorGUILayout.Toggle(
new GUIContent("Enable raycast target on Images", "Turn off for purely decorative layers"),
addRaycastTargets
);
preserveSortingOrder = EditorGUILayout.Toggle(
new GUIContent("Preserve sorting order", "Use sortingOrder to set sibling order"),
preserveSortingOrder
);
parentName = EditorGUILayout.TextField(
new GUIContent("Parent name", "Name of the generated UI root under the Canvas"),
parentName
);
// 新增 Toggle
exportAsPng = EditorGUILayout.Toggle(
new GUIContent("Export sprites as PNG", "If checked, each sprite will be exported as a PNG file"),
exportAsPng
);
EditorGUILayout.Space();
using (new EditorGUI.DisabledScope(sourceRoot == null))
{
if (GUILayout.Button("Convert to UGUI"))
{
Convert();
}
}
}
private void Convert()
{
if (sourceRoot == null)
{
ShowNotification(new GUIContent("Select a source root."));
return;
}
Canvas canvas = targetCanvas;
if (canvas == null && autoCreateCanvasIfMissing)
{
var canvasGO = new GameObject("Canvas", typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster));
canvas = canvasGO.GetComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
var scaler = canvasGO.GetComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1920, 1080);
scaler.referencePixelsPerUnit = referencePPU;
if (FindObjectOfType<UnityEngine.EventSystems.EventSystem>() == null)
{
var es = new GameObject("EventSystem", typeof(UnityEngine.EventSystems.EventSystem), typeof(UnityEngine.EventSystems.StandaloneInputModule));
Undo.RegisterCreatedObjectUndo(es, "Create EventSystem");
}
Undo.RegisterCreatedObjectUndo(canvasGO, "Create Canvas");
}
if (canvas == null)
{
ShowNotification(new GUIContent("Provide a Canvas or enable auto-create."));
return;
}
GameObject uiRootGO = new GameObject(string.IsNullOrEmpty(parentName) ? "PSB_UI" : parentName, typeof(RectTransform));
Undo.RegisterCreatedObjectUndo(uiRootGO, "Create UI Root");
uiRootGO.transform.SetParent(canvas.transform, false);
var uiRootRT = uiRootGO.GetComponent<RectTransform>();
uiRootRT.anchorMin = new Vector2(0.5f, 0.5f);
uiRootRT.anchorMax = new Vector2(0.5f, 0.5f);
uiRootRT.pivot = new Vector2(0.5f, 0.5f);
uiRootRT.anchoredPosition = Vector2.zero;
uiRootRT.localScale = Vector3.one;
var mapping = new Dictionary<Transform, RectTransform>();
mapping[sourceRoot.transform] = uiRootRT;
var all = sourceRoot.GetComponentsInChildren<Transform>(true)
.OrderBy(t => t == sourceRoot.transform ? -1 : 0)
.ThenBy(t => t.GetSiblingIndex())
.ToList();
foreach (var t in all)
{
if (t == sourceRoot.transform) continue;
var parent = t.parent;
if (!mapping.TryGetValue(parent, out var parentRT))
{
Debug.LogWarning($"Parent mapping missing for {t.name}. Skipping.");
continue;
}
var containerGO = new GameObject(t.name, typeof(RectTransform));
Undo.RegisterCreatedObjectUndo(containerGO, "Create UI Container");
var rt = containerGO.GetComponent<RectTransform>();
containerGO.transform.SetParent(parentRT, false);
rt.localPosition = t.localPosition * referencePPU;
rt.localRotation = t.localRotation;
rt.localScale = t.localScale;
rt.anchorMin = new Vector2(0.5f, 0.5f);
rt.anchorMax = new Vector2(0.5f, 0.5f);
rt.pivot = new Vector2(0.5f, 0.5f);
mapping[t] = rt;
}
// Create UI Images for SpriteRenderers
foreach (var kv in mapping.ToList())
{
var src = kv.Key;
var dstRT = kv.Value;
var sr = src.GetComponent<SpriteRenderer>();
if (sr == null || sr.sprite == null)
continue;
Sprite copiedSprite = null;
if (exportAsPng)
{
// ===== 导出 PNG 文件(修复尺寸问题)=====
copiedSprite = ExportSpriteAsPNG(sr.sprite, sourceRoot);
if (copiedSprite == null)
{
Debug.LogWarning($"导出 PNG 失败,使用原始 Sprite。源对象: {src.name}");
copiedSprite = sr.sprite;
}
}
else
{
copiedSprite = sr.sprite;
if (copiedSprite == null)
{
Debug.LogWarning($"原始 Sprite 为空,跳过。源对象: {src.name}");
continue;
}
}
// Add Image
var img = dstRT.gameObject.AddComponent<Image>();
img.sprite = copiedSprite;
img.preserveAspect = false;
img.raycastTarget = addRaycastTargets;
if (copyAlphaFromRenderer)
{
var c = img.color;
c.a = sr.color.a;
img.color = c;
}
img.SetNativeSize();
dstRT.pivot = copiedSprite.pivot / new Vector2(copiedSprite.rect.width, copiedSprite.rect.height);
}
if (preserveSortingOrder)
{
var parents = mapping.Values.Select(rt => rt.parent).Distinct().Where(p => p != null);
foreach (var p in parents)
{
var children = new List<RectTransform>();
for (int i = 0; i < p.childCount; i++)
{
children.Add(p.GetChild(i) as RectTransform);
}
children = children
.OrderBy(rt =>
{
var correspondingSrc = mapping.FirstOrDefault(x => x.Value == rt).Key;
var sr = correspondingSrc != null ? correspondingSrc.GetComponent<SpriteRenderer>() : null;
int order = sr != null ? sr.sortingOrder : 0;
return order;
})
.ThenBy(rt => rt.name)
.ToList();
for (int i = 0; i < children.Count; i++)
{
children[i].SetSiblingIndex(i);
}
}
}
Selection.activeObject = uiRootGO;
EditorGUIUtility.PingObject(uiRootGO);
Debug.Log(exportAsPng
? "PSB → UGUI conversion completed (sprites exported as PNG and assigned)."
: "PSB → UGUI conversion completed (using original psb sprites).");
}
/// <summary>
/// 将 Sprite 导出为 PNG 文件,并保持原始尺寸和 PPU 设置
/// </summary>
private Sprite ExportSpriteAsPNG(Sprite originalSprite, GameObject sourceRoot)
{
try
{
// 获取原始 Sprite 的信息
Texture2D srcTex = originalSprite.texture;
Rect rect = originalSprite.rect;
int w = Mathf.RoundToInt(rect.width);
int h = Mathf.RoundToInt(rect.height);
// 获取原始 PPU 和 pivot
float originalPPU = originalSprite.pixelsPerUnit;
Vector2 pivotNormalized = new Vector2(
originalSprite.pivot.x / rect.width,
originalSprite.pivot.y / rect.height
);
// 确定保存路径
Object prefab = PrefabUtility.GetCorrespondingObjectFromSource(sourceRoot);
string psbPath = AssetDatabase.GetAssetPath(prefab);
if (string.IsNullOrEmpty(psbPath))
{
psbPath = AssetDatabase.GetAssetPath(originalSprite);
}
string psbFolder = Path.GetDirectoryName(psbPath);
if (string.IsNullOrEmpty(psbFolder))
{
Debug.LogError($"无法获取有效的 psb 文件夹路径,源对象: {sourceRoot.name}");
return null;
}
string psbName = Path.GetFileNameWithoutExtension(psbPath);
string targetFolder = Path.Combine(psbFolder, psbName);
if (!AssetDatabase.IsValidFolder(targetFolder))
{
AssetDatabase.CreateFolder(psbFolder, psbName);
}
// 使用 RenderTexture 导出 PNG
RenderTexture rt = RenderTexture.GetTemporary(srcTex.width, srcTex.height, 0, RenderTextureFormat.ARGB32);
Graphics.Blit(srcTex, rt);
RenderTexture.active = rt;
Texture2D outTex = new Texture2D(w, h, TextureFormat.RGBA32, false);
outTex.ReadPixels(new Rect(rect.x, rect.y, rect.width, rect.height), 0, 0);
outTex.Apply();
RenderTexture.active = null;
RenderTexture.ReleaseTemporary(rt);
// 保存 PNG 文件
byte[] pngData = outTex.EncodeToPNG();
string pngPath = Path.Combine(targetFolder, originalSprite.name + ".png");
pngPath = AssetDatabase.GenerateUniqueAssetPath(pngPath);
if (string.IsNullOrEmpty(pngPath))
{
Debug.LogWarning($"PNG 路径为空,跳过保存。源对象: {sourceRoot.name}");
return null;
}
File.WriteAllBytes(pngPath, pngData);
DestroyImmediate(outTex);
// 导入并配置纹理
AssetDatabase.ImportAsset(pngPath);
var texImp = AssetImporter.GetAtPath(pngPath) as TextureImporter;
if (texImp != null)
{
texImp.textureType = TextureImporterType.Sprite;
texImp.spritePixelsPerUnit = originalPPU; // 关键:使用原始 PPU
texImp.alphaIsTransparency = true;
texImp.spritePivot = pivotNormalized;
texImp.mipmapEnabled = false;
texImp.filterMode = FilterMode.Bilinear;
texImp.textureCompression = TextureImporterCompression.Uncompressed;
texImp.SaveAndReimport();
}
Sprite result = AssetDatabase.LoadAssetAtPath<Sprite>(pngPath);
if (result == null)
{
Debug.LogError($"无法加载 Sprite: {pngPath}");
return null;
}
return result;
}
catch (System.Exception e)
{
Debug.LogError($"导出 PNG 时发生错误: {e.Message}\n{e.StackTrace}");
return null;
}
}
}
#endregion
#region 多线程Jobs
public class PsbToUGUIWindowOptimized : EditorWindow
{
[Header("Inputs")]
private GameObject sourceRoot;
private Canvas targetCanvas;
[Header("Config")]
private float referencePPU = 100f;
private bool copyAlphaFromRenderer = true;
private bool preserveSortingOrder = true;
private bool addRaycastTargets = false;
private bool autoCreateCanvasIfMissing = true;
private string parentName = "PSB_UI";
private bool exportAsPng = false;
// 性能优化选项
private bool useParallelExport = true;
private int maxParallelTasks = 4;
[MenuItem("Tools/PSB → UGUI Converter (Jobs)")]
public static void ShowWindow()
{
var win = GetWindow<PsbToUGUIWindowOptimized>("PSB → UGUI (Jobs)");
win.minSize = new Vector2(450, 420);
}
private void OnGUI()
{
EditorGUILayout.LabelField("Source", EditorStyles.boldLabel);
sourceRoot = (GameObject)EditorGUILayout.ObjectField(
new GUIContent("PSB root (scene GameObject)", "Select the instantiated PSB root that contains SpriteRenderers"),
sourceRoot, typeof(GameObject), true);
EditorGUILayout.Space();
EditorGUILayout.LabelField("Target", EditorStyles.boldLabel);
targetCanvas = (Canvas)EditorGUILayout.ObjectField(
new GUIContent("Target Canvas", "Canvas to place generated UI under"),
targetCanvas, typeof(Canvas), true);
autoCreateCanvasIfMissing = EditorGUILayout.Toggle(
new GUIContent("Auto-create canvas if missing", "Create an Overlay Canvas+EventSystem when none provided"),
autoCreateCanvasIfMissing
);
EditorGUILayout.Space();
EditorGUILayout.LabelField("Conversion settings", EditorStyles.boldLabel);
referencePPU = EditorGUILayout.FloatField(
new GUIContent("Reference Pixels Per Unit", "Use the PSD Importer PPU (often 100). Controls world→UI pixel mapping"),
referencePPU
);
copyAlphaFromRenderer = EditorGUILayout.Toggle(
new GUIContent("Copy alpha from SpriteRenderer color", "Image color.a = SpriteRenderer.color.a"),
copyAlphaFromRenderer
);
addRaycastTargets = EditorGUILayout.Toggle(
new GUIContent("Enable raycast target on Images", "Turn off for purely decorative layers"),
addRaycastTargets
);
preserveSortingOrder = EditorGUILayout.Toggle(
new GUIContent("Preserve sorting order", "Use sortingOrder to set sibling order"),
preserveSortingOrder
);
parentName = EditorGUILayout.TextField(
new GUIContent("Parent name", "Name of the generated UI root under the Canvas"),
parentName
);
EditorGUILayout.Space();
EditorGUILayout.LabelField("Performance", EditorStyles.boldLabel);
exportAsPng = EditorGUILayout.Toggle(
new GUIContent("Export sprites as PNG", "If checked, each sprite will be exported as a PNG file"),
exportAsPng
);
if (exportAsPng)
{
useParallelExport = EditorGUILayout.Toggle(
new GUIContent("Parallel export (faster)", "Use multiple threads to export PNGs simultaneously"),
useParallelExport
);
if (useParallelExport)
{
maxParallelTasks = EditorGUILayout.IntSlider(
new GUIContent("Max parallel tasks", "Number of concurrent export threads"),
maxParallelTasks, 1, 8
);
}
}
EditorGUILayout.Space();
using (new EditorGUI.DisabledScope(sourceRoot == null))
{
if (GUILayout.Button("Convert to UGUI (Optimized)"))
{
Convert();
}
}
}
private void Convert()
{
if (sourceRoot == null)
{
ShowNotification(new GUIContent("Select a source root."));
return;
}
EditorUtility.DisplayProgressBar("Converting to UGUI (Optimized)", "Setting up canvas...", 0f);
try
{
Canvas canvas = SetupCanvas();
if (canvas == null) return;
GameObject uiRootGO = CreateUIRoot(canvas);
var mapping = CreateTransformMapping(uiRootGO);
EditorUtility.DisplayProgressBar("Converting to UGUI (Optimized)", "Collecting sprite data...", 0.2f);
// 收集所有需要处理的SpriteRenderer(在主线程)
var spriteDataList = new List<SpriteExportData>();
foreach (var kv in mapping)
{
var sr = kv.Key.GetComponent<SpriteRenderer>();
if (sr != null && sr.sprite != null)
{
spriteDataList.Add(new SpriteExportData
{
src = kv.Key,
dstRT = kv.Value,
sr = sr,
originalSprite = sr.sprite,
spriteName = sr.sprite.name,
// 在主线程获取所有需要的数据
texture = sr.sprite.texture,
rect = sr.sprite.rect,
originalPPU = sr.sprite.pixelsPerUnit,
pivot = sr.sprite.pivot
});
}
}
var spriteMapping = new ConcurrentDictionary<Transform, Sprite>();
if (exportAsPng && useParallelExport && spriteDataList.Count > 0)
{
EditorUtility.DisplayProgressBar("Converting to UGUI (Optimized)", "Exporting PNGs (parallel)...", 0.3f);
// 准备并行任务
var tasks = new List<Task<(Transform src, string pngPath, SpriteExportData data)>>();
var semaphore = new SemaphoreSlim(maxParallelTasks);
foreach (var data in spriteDataList)
{
tasks.Add(Task.Run(() => ExportSpritePNGAsync(data, semaphore)));
}
// 等待所有导出任务完成
Task.WaitAll(tasks.ToArray());
// 在主线程导入所有PNG
EditorUtility.DisplayProgressBar("Converting to UGUI (Optimized)", "Importing PNGs...", 0.6f);
foreach (var task in tasks)
{
var result = task.Result;
if (!string.IsNullOrEmpty(result.pngPath))
{
var importedSprite = ImportPNGAsSprite(result.pngPath, result.data);
if (importedSprite != null)
{
spriteMapping[result.src] = importedSprite;
}
else
{
spriteMapping[result.src] = result.data.originalSprite;
}
}
else
{
spriteMapping[result.src] = result.data.originalSprite;
}
}
}
else
{
// 顺序处理
for (int i = 0; i < spriteDataList.Count; i++)
{
var data = spriteDataList[i];
float progress = 0.3f + (0.5f * i / spriteDataList.Count);
EditorUtility.DisplayProgressBar("Converting to UGUI (Optimized)", $"Processing {data.spriteName}...", progress);
Sprite targetSprite = data.originalSprite;
if (exportAsPng)
{
targetSprite = ExportSpriteAsPNGSequential(data) ?? targetSprite;
}
spriteMapping[data.src] = targetSprite;
}
}
EditorUtility.DisplayProgressBar("Converting to UGUI (Optimized)", "Creating UI components...", 0.8f);
// 创建UI组件(必须在主线程)
foreach (var data in spriteDataList)
{
if (spriteMapping.TryGetValue(data.src, out var targetSprite))
{
CreateUIImage(data.dstRT, targetSprite, data.sr);
}
}
if (preserveSortingOrder)
{
ApplySortingOrder(mapping);
}
Selection.activeObject = uiRootGO;
EditorGUIUtility.PingObject(uiRootGO);
Debug.Log(exportAsPng
? $"PSB → UGUI conversion completed (sprites exported as PNG using {(useParallelExport ? "parallel" : "sequential")} mode)."
: "PSB → UGUI conversion completed (using original psb sprites).");
}
finally
{
EditorUtility.ClearProgressBar();
}
}
private Canvas SetupCanvas()
{
Canvas canvas = targetCanvas;
if (canvas == null && autoCreateCanvasIfMissing)
{
var canvasGO = new GameObject("Canvas", typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster));
canvas = canvasGO.GetComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
var scaler = canvasGO.GetComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1920, 1080);
scaler.referencePixelsPerUnit = referencePPU;
if (FindObjectOfType<UnityEngine.EventSystems.EventSystem>() == null)
{
var es = new GameObject("EventSystem", typeof(UnityEngine.EventSystems.EventSystem), typeof(UnityEngine.EventSystems.StandaloneInputModule));
Undo.RegisterCreatedObjectUndo(es, "Create EventSystem");
}
Undo.RegisterCreatedObjectUndo(canvasGO, "Create Canvas");
}
if (canvas == null)
{
ShowNotification(new GUIContent("Provide a Canvas or enable auto-create."));
return null;
}
return canvas;
}
private GameObject CreateUIRoot(Canvas canvas)
{
GameObject uiRootGO = new GameObject(string.IsNullOrEmpty(parentName) ? "PSB_UI" : parentName, typeof(RectTransform));
Undo.RegisterCreatedObjectUndo(uiRootGO, "Create UI Root");
uiRootGO.transform.SetParent(canvas.transform, false);
var uiRootRT = uiRootGO.GetComponent<RectTransform>();
uiRootRT.anchorMin = new Vector2(0.5f, 0.5f);
uiRootRT.anchorMax = new Vector2(0.5f, 0.5f);
uiRootRT.pivot = new Vector2(0.5f, 0.5f);
uiRootRT.anchoredPosition = Vector2.zero;
uiRootRT.localScale = Vector3.one;
return uiRootGO;
}
private Dictionary<Transform, RectTransform> CreateTransformMapping(GameObject uiRootGO)
{
var mapping = new Dictionary<Transform, RectTransform>();
mapping[sourceRoot.transform] = uiRootGO.GetComponent<RectTransform>();
var all = sourceRoot.GetComponentsInChildren<Transform>(true)
.OrderBy(t => t == sourceRoot.transform ? -1 : 0)
.ThenBy(t => t.GetSiblingIndex())
.ToList();
foreach (var t in all)
{
if (t == sourceRoot.transform) continue;
if (!mapping.TryGetValue(t.parent, out var parentRT))
{
Debug.LogWarning($"Parent mapping missing for {t.name}. Skipping.");
continue;
}
var containerGO = new GameObject(t.name, typeof(RectTransform));
Undo.RegisterCreatedObjectUndo(containerGO, "Create UI Container");
var rt = containerGO.GetComponent<RectTransform>();
containerGO.transform.SetParent(parentRT, false);
rt.localPosition = t.localPosition * referencePPU;
rt.localRotation = t.localRotation;
rt.localScale = t.localScale;
rt.anchorMin = new Vector2(0.5f, 0.5f);
rt.anchorMax = new Vector2(0.5f, 0.5f);
rt.pivot = new Vector2(0.5f, 0.5f);
mapping[t] = rt;
}
return mapping;
}
private void CreateUIImage(RectTransform dstRT, Sprite sprite, SpriteRenderer sr)
{
var img = dstRT.gameObject.AddComponent<Image>();
img.sprite = sprite;
img.preserveAspect = false;
img.raycastTarget = addRaycastTargets;
if (copyAlphaFromRenderer)
{
var c = img.color;
c.a = sr.color.a;
img.color = c;
}
img.SetNativeSize();
dstRT.pivot = sprite.pivot / new Vector2(sprite.rect.width, sprite.rect.height);
}
private void ApplySortingOrder(Dictionary<Transform, RectTransform> mapping)
{
var parents = mapping.Values.Select(rt => rt.parent).Distinct().Where(p => p != null);
foreach (var p in parents)
{
var children = new List<RectTransform>();
for (int i = 0; i < p.childCount; i++)
{
children.Add(p.GetChild(i) as RectTransform);
}
children = children
.OrderBy(rt =>
{
var correspondingSrc = mapping.FirstOrDefault(x => x.Value == rt).Key;
var sr = correspondingSrc != null ? correspondingSrc.GetComponent<SpriteRenderer>() : null;
return sr != null ? sr.sortingOrder : 0;
})
.ThenBy(rt => rt.name)
.ToList();
for (int i = 0; i < children.Count; i++)
{
children[i].SetSiblingIndex(i);
}
}
}
// 异步导出PNG(在工作线程执行编码)
private (Transform src, string pngPath, SpriteExportData data) ExportSpritePNGAsync(SpriteExportData data, SemaphoreSlim semaphore)
{
semaphore.Wait();
try
{
string pngPath = ExportSpritePNGFromData(data);
return (data.src, pngPath, data);
}
finally
{
semaphore.Release();
}
}
// 从已收集的数据导出PNG(可以在工作线程执行)
private string ExportSpritePNGFromData(SpriteExportData data)
{
try
{
int w = Mathf.RoundToInt(data.rect.width);
int h = Mathf.RoundToInt(data.rect.height);
// 读取纹理数据
Color[] pixels = data.texture.GetPixels((int)data.rect.x, (int)data.rect.y, w, h);
// 创建纹理并编码
Texture2D outTex = new Texture2D(w, h, TextureFormat.RGBA32, false);
outTex.SetPixels(pixels);
outTex.Apply();
byte[] pngData = outTex.EncodeToPNG();
// 确定保存路径
string targetFolder = GetOrCreateTargetFolder(data);
if (string.IsNullOrEmpty(targetFolder))
{
UnityEngine.Object.DestroyImmediate(outTex);
return null;
}
string pngPath = Path.Combine(targetFolder, data.spriteName + ".png");
// 确保文件名唯一
int counter = 1;
string originalPath = pngPath;
while (File.Exists(pngPath))
{
pngPath = Path.Combine(targetFolder, $"{data.spriteName}_{counter}.png");
counter++;
}
File.WriteAllBytes(pngPath, pngData);
UnityEngine.Object.DestroyImmediate(outTex);
return pngPath;
}
catch (System.Exception e)
{
Debug.LogError($"导出 PNG 时发生错误 ({data.spriteName}): {e.Message}");
return null;
}
}
// 在主线程导入PNG为Sprite
private Sprite ImportPNGAsSprite(string pngPath, SpriteExportData data)
{
try
{
AssetDatabase.ImportAsset(pngPath);
var texImp = AssetImporter.GetAtPath(pngPath) as TextureImporter;
if (texImp != null)
{
texImp.textureType = TextureImporterType.Sprite;
texImp.spritePixelsPerUnit = data.originalPPU;
texImp.alphaIsTransparency = true;
Vector2 pivotNormalized = new Vector2(
data.pivot.x / data.rect.width,
data.pivot.y / data.rect.height
);
texImp.spritePivot = pivotNormalized;
texImp.mipmapEnabled = false;
texImp.filterMode = FilterMode.Bilinear;
texImp.textureCompression = TextureImporterCompression.Uncompressed;
texImp.SaveAndReimport();
}
return AssetDatabase.LoadAssetAtPath<Sprite>(pngPath);
}
catch (System.Exception e)
{
Debug.LogError($"导入 PNG 时发生错误 ({pngPath}): {e.Message}");
return null;
}
}
// 顺序版本 - 完整功能
private Sprite ExportSpriteAsPNGSequential(SpriteExportData data)
{
try
{
int w = Mathf.RoundToInt(data.rect.width);
int h = Mathf.RoundToInt(data.rect.height);
RenderTexture rt = RenderTexture.GetTemporary(data.texture.width, data.texture.height, 0, RenderTextureFormat.ARGB32);
Graphics.Blit(data.texture, rt);
RenderTexture.active = rt;
Texture2D outTex = new Texture2D(w, h, TextureFormat.RGBA32, false);
outTex.ReadPixels(new Rect(data.rect.x, data.rect.y, data.rect.width, data.rect.height), 0, 0);
outTex.Apply();
RenderTexture.active = null;
RenderTexture.ReleaseTemporary(rt);
byte[] pngData = outTex.EncodeToPNG();
string targetFolder = GetOrCreateTargetFolder(data);
if (string.IsNullOrEmpty(targetFolder))
{
UnityEngine.Object.DestroyImmediate(outTex);
return null;
}
string pngPath = Path.Combine(targetFolder, data.spriteName + ".png");
pngPath = AssetDatabase.GenerateUniqueAssetPath(pngPath);
if (string.IsNullOrEmpty(pngPath))
{
UnityEngine.Object.DestroyImmediate(outTex);
return null;
}
File.WriteAllBytes(pngPath, pngData);
UnityEngine.Object.DestroyImmediate(outTex);
AssetDatabase.ImportAsset(pngPath);
var texImp = AssetImporter.GetAtPath(pngPath) as TextureImporter;
if (texImp != null)
{
texImp.textureType = TextureImporterType.Sprite;
texImp.spritePixelsPerUnit = data.originalPPU;
texImp.alphaIsTransparency = true;
Vector2 pivotNormalized = new Vector2(
data.pivot.x / data.rect.width,
data.pivot.y / data.rect.height
);
texImp.spritePivot = pivotNormalized;
texImp.mipmapEnabled = false;
texImp.filterMode = FilterMode.Bilinear;
texImp.textureCompression = TextureImporterCompression.Uncompressed;
texImp.SaveAndReimport();
}
return AssetDatabase.LoadAssetAtPath<Sprite>(pngPath);
}
catch (System.Exception e)
{
Debug.LogError($"导出 PNG 时发生错误 ({data.spriteName}): {e.Message}");
return null;
}
}
private string GetOrCreateTargetFolder(SpriteExportData data)
{
Object prefab = PrefabUtility.GetCorrespondingObjectFromSource(sourceRoot);
string psbPath = AssetDatabase.GetAssetPath(prefab);
if (string.IsNullOrEmpty(psbPath))
{
psbPath = AssetDatabase.GetAssetPath(data.originalSprite);
}
string psbFolder = Path.GetDirectoryName(psbPath);
if (string.IsNullOrEmpty(psbFolder))
{
Debug.LogError($"无法获取有效的 psb 文件夹路径,源对象: {sourceRoot.name}");
return null;
}
string psbName = Path.GetFileNameWithoutExtension(psbPath);
string targetFolder = Path.Combine(psbFolder, psbName);
// 确保在主线程创建文件夹
if (!AssetDatabase.IsValidFolder(targetFolder))
{
// 这个操作必须在主线程,所以如果当前在子线程就返回null
if (System.Threading.Thread.CurrentThread.ManagedThreadId != 1)
{
return null;
}
AssetDatabase.CreateFolder(psbFolder, psbName);
}
return targetFolder;
}
// 数据容器,用于传递Sprite信息
private class SpriteExportData
{
public Transform src;
public RectTransform dstRT;
public SpriteRenderer sr;
public Sprite originalSprite;
public string spriteName;
public Texture2D texture;
public Rect rect;
public float originalPPU;
public Vector2 pivot;
}
}
#endregion
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 6da7122f5a2be36409e67396174068c0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: