特效处理
This commit is contained in:
@@ -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:
|
||||
-28
@@ -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
|
||||
}
|
||||
-7
@@ -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:
|
||||
-28
@@ -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
|
||||
}
|
||||
-7
@@ -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
|
||||
}
|
||||
-7
@@ -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:
|
||||
Reference in New Issue
Block a user