TH1/Unity/Assets/Scripts/TH1_Logic/Editor/SpriteAssetEditorWindow.cs
2026-06-10 11:58:18 +08:00

898 lines
36 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* @Author: Claude
* @Description: TMP 表情图集自动化生成编辑器,将碎图打包为 TMP_SpriteAsset
* @Date: 2026年04月03日
* @Modify:
*/
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using TMPro;
using UnityEditor;
using UnityEngine;
using UnityEngine.TextCore;
namespace Logic.Editor
{
/// <summary>
/// TMP 表情图集自动化生成器
/// 功能流程:检查碎图 → 合并大图 → 设置切分 → 生成 TMP_SpriteAsset
/// </summary>
public class SpriteAssetEditorWindow : EditorWindow
{
// ============================================================
// 常量
// ============================================================
private static readonly Regex ValidNameRegex = new Regex(@"^[a-z0-9_]+$", RegexOptions.Compiled);
private static readonly int[] AtlasSizeOptions = { 1024, 2048, 4096 };
private static readonly string[] AtlasSizeLabels = { "1024", "2048", "4096" };
private const string DefaultSourceFolder = "Assets/BundleResources/TH1UI/Icon/Emoji";
private const string DefaultOutputFolder = "Assets/BundleResources/SpriteAsset";
// ============================================================
// 配置字段
// ============================================================
private DefaultAsset _sourceFolder;
private DefaultAsset _outputFolder;
private string _assetName = "EmojiSpriteAsset";
private int _atlasSizeIndex = 1; // 默认 2048
private int _padding = 2;
private bool _checkSize;
private int _expectedWidth = 128;
private int _expectedHeight = 128;
// ============================================================
// UI 状态
// ============================================================
private Vector2 _barPosition;
private Vector2 _logScrollPosition;
private string _logContent = "";
private GUIStyle _boxStyle;
private GUIStyle _logStyle;
// ============================================================
// 数据结构
// ============================================================
/// <summary>
/// 通过验证的碎图信息
/// </summary>
public class ValidSpriteInfo
{
public string FilePath; // 完整文件路径
public string AssetPath; // Assets/ 相对路径
public string Name; // 文件名(不含后缀)
public Texture2D Texture; // 加载后的纹理引用
}
// ============================================================
// 编辑器入口
// ============================================================
[MenuItem("Tools/表情图集生成器")]
private static void ShowWindow()
{
var window = CreateWindow<SpriteAssetEditorWindow>();
window.titleContent = new GUIContent("表情图集生成器");
window.minSize = new Vector2(500, 600);
window.Show();
}
private void OnEnable()
{
// 首次打开时自动填入默认文件夹
if (_sourceFolder == null)
_sourceFolder = AssetDatabase.LoadAssetAtPath<DefaultAsset>(DefaultSourceFolder);
if (_outputFolder == null)
_outputFolder = AssetDatabase.LoadAssetAtPath<DefaultAsset>(DefaultOutputFolder);
}
// ============================================================
// GUI 绘制
// ============================================================
private void OnGUI()
{
// 初始化样式
if (_boxStyle == null)
{
_boxStyle = InspectorUtils.GetHelpBoxStyle();
InspectorUtils.AddBorder(_boxStyle, new Color(0.5f, 0.5f, 0.5f));
}
if (_logStyle == null)
{
_logStyle = new GUIStyle(EditorStyles.textArea)
{
richText = true,
wordWrap = true,
fontSize = 11
};
}
_barPosition = EditorGUILayout.BeginScrollView(_barPosition);
// ---- 标题 ----
EditorGUILayout.Space(8);
EditorGUILayout.LabelField("TMP 表情图集生成器", EditorStyles.boldLabel);
InspectorUtils.DrawDivider(Color.gray, 1);
EditorGUILayout.Space(4);
// ---- 源文件夹 ----
EditorGUILayout.BeginVertical(_boxStyle);
EditorGUILayout.LabelField("基本设置", EditorStyles.boldLabel);
EditorGUILayout.Space(4);
EditorGUILayout.BeginHorizontal();
InspectorUtils.InspectorTextWidthRich("<b>源文件夹:</b>");
_sourceFolder = (DefaultAsset)EditorGUILayout.ObjectField(
_sourceFolder, typeof(DefaultAsset), false, GUILayout.Width(300));
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
InspectorUtils.InspectorTextWidthRich("<b>输出文件夹:</b>");
_outputFolder = (DefaultAsset)EditorGUILayout.ObjectField(
_outputFolder, typeof(DefaultAsset), false, GUILayout.Width(300));
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
InspectorUtils.InspectorTextWidthRich("<b>资产名称:</b>");
_assetName = EditorGUILayout.TextField(_assetName, GUILayout.Width(300));
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
EditorGUILayout.Space(4);
// ---- 图集设置 ----
EditorGUILayout.BeginVertical(_boxStyle);
EditorGUILayout.LabelField("图集设置", EditorStyles.boldLabel);
EditorGUILayout.Space(4);
EditorGUILayout.BeginHorizontal();
InspectorUtils.InspectorTextWidthRich("<b>最大尺寸:</b>");
_atlasSizeIndex = EditorGUILayout.Popup(_atlasSizeIndex, AtlasSizeLabels, GUILayout.Width(100));
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
InspectorUtils.InspectorTextWidthRich("<b>图元间距:</b>");
_padding = EditorGUILayout.IntField(_padding, GUILayout.Width(100));
_padding = Mathf.Clamp(_padding, 0, 16);
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(4);
// 尺寸检查
EditorGUILayout.BeginHorizontal();
_checkSize = InspectorUtils.InspectorToggleVertical("检查图片尺寸", _checkSize);
EditorGUILayout.EndHorizontal();
if (_checkSize)
{
EditorGUILayout.BeginHorizontal();
InspectorUtils.InspectorTextWidthRich(" <b>期望宽度:</b>");
_expectedWidth = EditorGUILayout.IntField(_expectedWidth, GUILayout.Width(100));
InspectorUtils.InspectorTextWidthRich(" <b>期望高度:</b>");
_expectedHeight = EditorGUILayout.IntField(_expectedHeight, GUILayout.Width(100));
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndVertical();
EditorGUILayout.Space(4);
// ---- 操作按钮 ----
EditorGUILayout.BeginVertical(_boxStyle);
EditorGUILayout.LabelField("操作", EditorStyles.boldLabel);
EditorGUILayout.Space(4);
EditorGUILayout.BeginHorizontal();
if (InspectorUtils.InspectorButtonWithTextWidth("检查图片资源"))
{
OnClickValidate();
}
EditorGUILayout.Space(10);
if (InspectorUtils.InspectorButtonWithTextWidth("一键生成 SpriteAsset"))
{
OnClickGenerate();
}
EditorGUILayout.Space(10);
if (InspectorUtils.InspectorButtonWithTextWidth("清空日志"))
{
_logContent = "";
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(4);
EditorGUILayout.EndVertical();
EditorGUILayout.Space(4);
// ---- 日志输出 ----
EditorGUILayout.BeginVertical(_boxStyle);
EditorGUILayout.LabelField("日志输出", EditorStyles.boldLabel);
EditorGUILayout.Space(4);
_logScrollPosition = EditorGUILayout.BeginScrollView(
_logScrollPosition, GUILayout.Height(250));
EditorGUILayout.TextArea(_logContent, _logStyle, GUILayout.ExpandHeight(true));
EditorGUILayout.EndScrollView();
EditorGUILayout.EndVertical();
EditorGUILayout.EndScrollView();
}
// ============================================================
// 按钮回调
// ============================================================
/// <summary>
/// 仅检查图片资源
/// </summary>
private void OnClickValidate()
{
_logContent = "";
AppendLog("<b>===== 开始检查图片资源 =====</b>");
string sourcePath = GetFolderPath(_sourceFolder, "源文件夹");
if (sourcePath == null) return;
var result = ValidateImages(sourcePath);
if (result != null)
{
AppendLog($"\n<color=green><b>检查完成,共 {result.Count} 张有效图片</b></color>");
}
}
/// <summary>
/// 一键生成完整流程
/// </summary>
private void OnClickGenerate()
{
_logContent = "";
AppendLog("<b>===== 开始一键生成 SpriteAsset =====</b>");
// 校验路径
string sourcePath = GetFolderPath(_sourceFolder, "源文件夹");
if (sourcePath == null) return;
string outputPath = GetFolderPath(_outputFolder, "输出文件夹");
if (outputPath == null) return;
if (string.IsNullOrWhiteSpace(_assetName))
{
AppendLog("<color=red>错误:资产名称不能为空</color>");
EditorUtility.DisplayDialog("错误", "资产名称不能为空。", "确定");
return;
}
// Step 1: 检查图片
AppendLog("\n<b>--- Step 1: 检查图片资源 ---</b>");
var validSprites = ValidateImages(sourcePath);
if (validSprites == null || validSprites.Count == 0)
{
AppendLog("<color=red>没有有效图片,流程终止</color>");
EditorUtility.DisplayDialog("错误", "没有找到有效的图片资源,流程终止。", "确定");
return;
}
// Step 2: 合并大图
AppendLog("\n<b>--- Step 2: 合并大图 ---</b>");
int atlasSize = AtlasSizeOptions[_atlasSizeIndex];
var packResult = PackAtlasTexture(validSprites, outputPath, atlasSize);
if (packResult == null)
{
AppendLog("<color=red>合并大图失败,流程终止</color>");
return;
}
// Step 3: 设置切分
AppendLog("\n<b>--- Step 3: 设置大图切分属性 ---</b>");
string atlasAssetPath = packResult.Value.atlasAssetPath;
var names = validSprites.Select(s => s.Name).ToArray();
bool sliceOk = SetupTextureImporter(
atlasAssetPath,
packResult.Value.rects,
names,
packResult.Value.atlasWidth,
packResult.Value.atlasHeight);
if (!sliceOk)
{
AppendLog("<color=red>设置切分失败,流程终止</color>");
return;
}
// Step 4: 生成 TMP_SpriteAsset
AppendLog("\n<b>--- Step 4: 生成 TMP_SpriteAsset ---</b>");
bool assetOk = GenerateTMPSpriteAsset(atlasAssetPath, outputPath);
if (!assetOk)
{
AppendLog("<color=red>生成 SpriteAsset 失败</color>");
return;
}
// Step 5: 自动设置 TMP Settings 的 Default Sprite Asset
AppendLog("\n<b>--- Step 5: 设置 TMP Settings ---</b>");
string generatedAssetPath = $"{outputPath}/{_assetName}.asset";
SetTMPDefaultSpriteAsset(generatedAssetPath);
AppendLog("\n<color=green><b>===== 全部流程完成!=====</b></color>");
EditorUtility.DisplayDialog("成功",
$"SpriteAsset 已生成并自动绑定!\n\n" +
$"图集:{atlasAssetPath}\n" +
$"资产:{generatedAssetPath}\n\n" +
$"已自动设置为 TMP Settings → Default Sprite Asset。\n" +
$"可直接在 TMP 文本中使用 <sprite name=\"xxx\"> 标签。",
"确定");
}
// ============================================================
// Step 1: 检查与收集图片资源
// ============================================================
/// <summary>
/// 验证源文件夹中的图片,返回通过验证的图片列表
/// </summary>
public List<ValidSpriteInfo> ValidateImages(string sourceFolderPath)
{
if (!Directory.Exists(sourceFolderPath))
{
AppendLog($"<color=red>错误:源文件夹不存在 - {sourceFolderPath}</color>");
return null;
}
var allFiles = Directory.GetFiles(sourceFolderPath);
if (allFiles.Length == 0)
{
AppendLog("<color=yellow>警告:源文件夹为空,没有找到任何文件</color>");
return new List<ValidSpriteInfo>();
}
var validList = new List<ValidSpriteInfo>();
int errorCount = 0;
foreach (var filePath in allFiles)
{
string fileName = Path.GetFileName(filePath);
string nameWithoutExt = Path.GetFileNameWithoutExtension(filePath);
string extension = Path.GetExtension(filePath).ToLower();
// 跳过 .meta 文件
if (extension == ".meta") continue;
// 检查后缀
if (extension != ".png")
{
AppendLog($"<color=yellow> 跳过非 PNG 文件:{fileName}</color>");
errorCount++;
continue;
}
// 检查文件名合法性
if (!IsValidSpriteName(nameWithoutExt))
{
AppendLog($"<color=red> 文件名不合法:{fileName}(仅允许小写英文、数字、下划线)</color>");
errorCount++;
continue;
}
// 检查重名
if (validList.Any(s => s.Name == nameWithoutExt))
{
AppendLog($"<color=red> 文件名重复:{fileName}</color>");
errorCount++;
continue;
}
// 转换为 Assets/ 相对路径
string assetPath = FilePathToAssetPath(filePath);
if (string.IsNullOrEmpty(assetPath))
{
AppendLog($"<color=red> 无法转换为 Asset 路径:{filePath}</color>");
errorCount++;
continue;
}
// 加载纹理并检查尺寸
var texture = AssetDatabase.LoadAssetAtPath<Texture2D>(assetPath);
if (texture == null)
{
AppendLog($"<color=red> 无法加载纹理:{assetPath}</color>");
errorCount++;
continue;
}
if (_checkSize)
{
if (texture.width != _expectedWidth || texture.height != _expectedHeight)
{
AppendLog($"<color=red> 尺寸不符:{fileName}{texture.width}x{texture.height},期望 {_expectedWidth}x{_expectedHeight}</color>");
errorCount++;
continue;
}
}
validList.Add(new ValidSpriteInfo
{
FilePath = filePath,
AssetPath = assetPath,
Name = nameWithoutExt,
Texture = texture
});
AppendLog($" <color=green>通过</color>{fileName}{texture.width}x{texture.height}");
}
AppendLog($"\n检查结果{validList.Count} 张有效,{errorCount} 张无效");
return validList;
}
/// <summary>
/// 检查 Sprite 名称是否合法(仅允许小写英文、数字、下划线)
/// </summary>
public static bool IsValidSpriteName(string name)
{
if (string.IsNullOrEmpty(name)) return false;
return ValidNameRegex.IsMatch(name);
}
// ============================================================
// Step 2: 自动合并大图
// ============================================================
/// <summary>
/// Pack 结果
/// </summary>
public struct PackResult
{
public string atlasAssetPath;
public Rect[] rects;
public int atlasWidth;
public int atlasHeight;
}
/// <summary>
/// 将碎图合并为一张大图并保存
/// </summary>
public PackResult? PackAtlasTexture(List<ValidSpriteInfo> sprites, string outputFolderPath, int maxAtlasSize)
{
try
{
// 确保所有碎图可读
AppendLog(" 设置碎图为可读状态...");
foreach (var sprite in sprites)
{
EnsureTextureReadable(sprite.AssetPath);
}
// 重新加载纹理Reimport 后引用可能变化)
var textures = new Texture2D[sprites.Count];
for (int i = 0; i < sprites.Count; i++)
{
textures[i] = AssetDatabase.LoadAssetAtPath<Texture2D>(sprites[i].AssetPath);
if (textures[i] == null)
{
AppendLog($"<color=red> 重新加载纹理失败:{sprites[i].AssetPath}</color>");
return null;
}
}
// 创建图集纹理并打包
AppendLog(" 开始 PackTextures...");
var atlas = new Texture2D(maxAtlasSize, maxAtlasSize, TextureFormat.RGBA32, false);
Rect[] rects = atlas.PackTextures(textures, _padding, maxAtlasSize);
if (rects == null || rects.Length != sprites.Count)
{
AppendLog("<color=red> PackTextures 失败:返回 Rect 数量不匹配</color>");
return null;
}
int atlasWidth = atlas.width;
int atlasHeight = atlas.height;
AppendLog($" 图集尺寸:{atlasWidth} x {atlasHeight}");
// 导出 PNG —— 先拷贝到干净的 RGBA32 纹理,防止压缩格式导致 EncodeToPNG 返回 null
string atlasFileName = $"{_assetName}_Atlas.png";
string outputAssetPath = $"{outputFolderPath}/{atlasFileName}";
string outputFullPath = AssetPathToFilePath(outputAssetPath);
var readableAtlas = new Texture2D(atlasWidth, atlasHeight, TextureFormat.RGBA32, false);
readableAtlas.SetPixels(atlas.GetPixels());
readableAtlas.Apply();
byte[] pngData = readableAtlas.EncodeToPNG();
DestroyImmediate(readableAtlas);
DestroyImmediate(atlas);
if (pngData == null)
{
AppendLog("<color=red> EncodeToPNG 失败:无法编码图集纹理</color>");
return null;
}
File.WriteAllBytes(outputFullPath, pngData);
AssetDatabase.Refresh();
AppendLog($" 图集已保存:{outputAssetPath}");
// 打印每张碎图在图集中的位置
for (int i = 0; i < sprites.Count; i++)
{
var r = rects[i];
int px = Mathf.RoundToInt(r.x * atlasWidth);
int py = Mathf.RoundToInt(r.y * atlasHeight);
int pw = Mathf.RoundToInt(r.width * atlasWidth);
int ph = Mathf.RoundToInt(r.height * atlasHeight);
AppendLog($" {sprites[i].Name} → ({px}, {py}, {pw}x{ph})");
}
return new PackResult
{
atlasAssetPath = outputAssetPath,
rects = rects,
atlasWidth = atlasWidth,
atlasHeight = atlasHeight
};
}
catch (Exception e)
{
AppendLog($"<color=red> PackTextures 异常:{e.Message}</color>");
Debug.LogException(e);
return null;
}
}
// ============================================================
// Step 3: 自动设置大图切分属性
// ============================================================
/// <summary>
/// 设置大图的 TextureImporter完成 Sprite Multiple 切分
/// </summary>
public bool SetupTextureImporter(string atlasAssetPath, Rect[] normalizedRects, string[] names,
int atlasWidth, int atlasHeight)
{
try
{
var importer = AssetImporter.GetAtPath(atlasAssetPath) as TextureImporter;
if (importer == null)
{
AppendLog($"<color=red> 无法获取 TextureImporter{atlasAssetPath}</color>");
return false;
}
// 设置贴图类型
importer.textureType = TextureImporterType.Sprite;
importer.spriteImportMode = SpriteImportMode.Multiple;
importer.isReadable = true;
importer.mipmapEnabled = false;
importer.textureCompression = TextureImporterCompression.Uncompressed;
importer.maxTextureSize = Mathf.Max(atlasWidth, atlasHeight);
// 构建切片数据
var spriteSheet = new SpriteMetaData[names.Length];
for (int i = 0; i < names.Length; i++)
{
var nr = normalizedRects[i];
// 将归一化 Rect 转为像素坐标(左下角坐标系)
var pixelRect = new Rect(
Mathf.RoundToInt(nr.x * atlasWidth),
Mathf.RoundToInt(nr.y * atlasHeight),
Mathf.RoundToInt(nr.width * atlasWidth),
Mathf.RoundToInt(nr.height * atlasHeight)
);
spriteSheet[i] = new SpriteMetaData
{
name = names[i],
rect = pixelRect,
alignment = (int)SpriteAlignment.Center,
pivot = new Vector2(0.5f, 0.5f)
};
AppendLog($" 切片[{i}]{names[i]} → rect({pixelRect.x}, {pixelRect.y}, {pixelRect.width}, {pixelRect.height})");
}
importer.spritesheet = spriteSheet;
importer.SaveAndReimport();
AppendLog(" TextureImporter 设置完成,已刷新资源");
return true;
}
catch (Exception e)
{
AppendLog($"<color=red> 设置 TextureImporter 异常:{e.Message}</color>");
Debug.LogException(e);
return false;
}
}
// ============================================================
// Step 4: 自动生成 TMP_SpriteAsset
// ============================================================
/// <summary>
/// 基于切分好的图集生成 TMP_SpriteAsset
/// </summary>
public bool GenerateTMPSpriteAsset(string atlasAssetPath, string outputFolderPath)
{
try
{
// 加载图集纹理
var atlasTexture = AssetDatabase.LoadAssetAtPath<Texture2D>(atlasAssetPath);
if (atlasTexture == null)
{
AppendLog($"<color=red> 无法加载图集纹理:{atlasAssetPath}</color>");
return false;
}
// 加载所有切分后的 Sprite
var sprites = AssetDatabase.LoadAllAssetsAtPath(atlasAssetPath)
.OfType<Sprite>().OrderBy(s => s.name).ToList();
if (sprites.Count == 0)
{
AppendLog("<color=red> 未找到切分后的 Sprite请确认 Step 3 是否正确执行</color>");
return false;
}
AppendLog($" 找到 {sprites.Count} 个 Sprite 切片");
// ====================================================================
// 严格按照 TMP 官方 TMP_SpriteAssetMenu.CreateSpriteAsset 的顺序构建
// ====================================================================
// 1. 先创建资产并持久化到磁盘(官方要求 CreateAsset 在设置属性之前)
var spriteAsset = ScriptableObject.CreateInstance<TMP_SpriteAsset>();
string assetPath = $"{outputFolderPath}/{_assetName}.asset";
AssetDatabase.CreateAsset(spriteAsset, assetPath);
// 2. 设置版本号和哈希version setter 是 internal需要反射
const BindingFlags bf = BindingFlags.NonPublic | BindingFlags.Instance;
typeof(TMP_SpriteAsset).GetField("m_Version", bf)?.SetValue(spriteAsset, "1.1.0");
spriteAsset.hashCode = TMP_TextUtilities.GetSimpleHashCode(spriteAsset.name);
// 3. 设置图集纹理引用
spriteAsset.spriteSheet = atlasTexture;
// 4. 构建 GlyphTable 和 CharacterTable使用原始像素值不做任何缩放
var spriteGlyphTable = new List<TMP_SpriteGlyph>();
var spriteCharacterTable = new List<TMP_SpriteCharacter>();
for (int i = 0; i < sprites.Count; i++)
{
var sprite = sprites[i];
var rect = sprite.rect;
// GlyphMetrics 使用原始像素值
// bearingX = 0不向左偏移避免覆盖前一字符
// bearingY = height图标底部对齐基线整体在基线上方与文字对齐
var spriteGlyph = new TMP_SpriteGlyph();
spriteGlyph.index = (uint)i;
spriteGlyph.sprite = sprite;
spriteGlyph.metrics = new GlyphMetrics(
rect.width, // width: 原始像素宽度
rect.height, // height: 原始像素高度
0, // bearingX: 无水平偏移
rect.height * 0.9f, // bearingY: 底部略低于基线,视觉居中
rect.width // advance: 光标前进量 = 图标宽度
);
spriteGlyph.glyphRect = new GlyphRect(
(int)rect.x,
(int)rect.y,
(int)rect.width,
(int)rect.height
);
spriteGlyph.scale = 1.0f;
spriteGlyph.atlasIndex = 0;
spriteGlyphTable.Add(spriteGlyph);
// SpriteCharacter: unicode 统一用 0xFFFE官方占位符通过 name 查找
var spriteCharacter = new TMP_SpriteCharacter(0xFFFE, spriteGlyph);
spriteCharacter.name = sprite.name;
spriteCharacter.scale = 1.0f;
spriteCharacterTable.Add(spriteCharacter);
AppendLog($" [{i}] {sprite.name} - rect({rect.x}, {rect.y}, {rect.width}x{rect.height})");
}
// 5. 通过反射写入表数据setter 是 internal
typeof(TMP_SpriteAsset).GetField("m_SpriteGlyphTable", bf)?.SetValue(spriteAsset, spriteGlyphTable);
typeof(TMP_SpriteAsset).GetField("m_SpriteCharacterTable", bf)?.SetValue(spriteAsset, spriteCharacterTable);
// 6. 创建材质并作为子资产嵌入(必须在 CreateAsset 之后)
Shader spriteShader = Shader.Find("TextMeshPro/Sprite");
if (spriteShader == null)
{
AppendLog("<color=red> 错误:找不到 TextMeshPro/Sprite shader</color>");
return false;
}
var material = new Material(spriteShader);
material.SetTexture(ShaderUtilities.ID_MainTex, spriteAsset.spriteSheet);
spriteAsset.material = material;
material.hideFlags = HideFlags.HideInHierarchy;
AssetDatabase.AddObjectToAsset(material, spriteAsset);
// 7. 更新查找表(构建 name → character 哈希字典)
spriteAsset.UpdateLookupTables();
// 8. 标脏并保存
EditorUtility.SetDirty(spriteAsset);
AssetDatabase.SaveAssets();
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(spriteAsset));
AppendLog($" <color=green>TMP_SpriteAsset 已保存:{assetPath}</color>");
AppendLog($" 共 {spriteCharacterTable.Count} 个 Sprite 字符映射");
return true;
}
catch (Exception e)
{
AppendLog($"<color=red> 生成 TMP_SpriteAsset 异常:{e.Message}</color>");
Debug.LogException(e);
return false;
}
}
// ============================================================
// Step 5: 自动设置 TMP Settings
// ============================================================
/// <summary>
/// 将生成的 SpriteAsset 自动设置为 TMP Settings 的 Default Sprite Asset
/// </summary>
private void SetTMPDefaultSpriteAsset(string spriteAssetPath)
{
try
{
var spriteAsset = AssetDatabase.LoadAssetAtPath<TMP_SpriteAsset>(spriteAssetPath);
if (spriteAsset == null)
{
AppendLog($"<color=red> 无法加载 SpriteAsset{spriteAssetPath}</color>");
return;
}
var tmpSettings = TMP_Settings.instance;
if (tmpSettings == null)
{
AppendLog("<color=red> 未找到 TMP Settings 实例,请先通过 Window → TextMeshPro → Settings 创建</color>");
return;
}
// TMP_Settings.defaultSpriteAsset 的 setter 是 internal通过 SerializedObject 修改
string settingsPath = AssetDatabase.GetAssetPath(tmpSettings);
var so = new SerializedObject(tmpSettings);
so.Update();
var prop = so.FindProperty("m_defaultSpriteAsset");
if (prop != null)
{
prop.objectReferenceValue = spriteAsset;
so.ApplyModifiedProperties();
EditorUtility.SetDirty(tmpSettings);
AssetDatabase.SaveAssets();
AppendLog($" <color=green>已自动设置 TMP Settings → Default Sprite Asset = {spriteAsset.name}</color>");
AppendLog($" TMP Settings 路径:{settingsPath}");
}
else
{
AppendLog("<color=yellow> 警告:未找到 m_defaultSpriteAsset 属性,请手动在 TMP Settings 中绑定</color>");
}
}
catch (Exception e)
{
AppendLog($"<color=yellow> 自动设置 TMP Settings 失败:{e.Message},请手动绑定</color>");
Debug.LogException(e);
}
}
// ============================================================
// 工具方法
// ============================================================
/// <summary>
/// 确保纹理为可读状态
/// </summary>
private void EnsureTextureReadable(string assetPath)
{
var importer = AssetImporter.GetAtPath(assetPath) as TextureImporter;
if (importer == null) return;
bool needsReimport = false;
if (!importer.isReadable)
{
importer.isReadable = true;
needsReimport = true;
}
// 必须为未压缩格式,否则 PackTextures 产出压缩图集EncodeToPNG 会返回 null
if (importer.textureCompression != TextureImporterCompression.Uncompressed)
{
importer.textureCompression = TextureImporterCompression.Uncompressed;
needsReimport = true;
}
if (needsReimport)
{
importer.SaveAndReimport();
AppendLog($" 已设置可读+未压缩:{Path.GetFileName(assetPath)}");
}
}
/// <summary>
/// 获取文件夹的 Assets/ 相对路径
/// </summary>
private string GetFolderPath(DefaultAsset folder, string label)
{
if (folder == null)
{
AppendLog($"<color=red>错误:请指定{label}</color>");
EditorUtility.DisplayDialog("错误", $"请先指定{label}。", "确定");
return null;
}
string path = AssetDatabase.GetAssetPath(folder);
if (string.IsNullOrEmpty(path) || !AssetDatabase.IsValidFolder(path))
{
AppendLog($"<color=red>错误:{label}路径无效 - {path}</color>");
EditorUtility.DisplayDialog("错误", $"{label}路径无效。", "确定");
return null;
}
return path;
}
/// <summary>
/// 将文件系统绝对路径转换为 Assets/ 相对路径
/// </summary>
private static string FilePathToAssetPath(string filePath)
{
string normalized = filePath.Replace('\\', '/');
string dataPath = Application.dataPath.Replace('\\', '/');
if (normalized.StartsWith(dataPath))
{
return "Assets" + normalized.Substring(dataPath.Length);
}
// 尝试查找 "Assets/" 子串
int idx = normalized.IndexOf("Assets/", StringComparison.OrdinalIgnoreCase);
if (idx >= 0)
{
return normalized.Substring(idx);
}
return null;
}
/// <summary>
/// 将 Assets/ 相对路径转换为文件系统绝对路径
/// </summary>
private static string AssetPathToFilePath(string assetPath)
{
string dataPath = Application.dataPath.Replace('\\', '/');
// Application.dataPath 以 "Assets" 结尾
string projectRoot = dataPath.Substring(0, dataPath.Length - "Assets".Length);
return projectRoot + assetPath;
}
/// <summary>
/// 追加日志到输出区
/// </summary>
public void AppendLog(string message)
{
_logContent += message + "\n";
Debug.Log($"[SpriteAsset] {message}");
Repaint();
}
}
}