using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text; using TMPro; using UnityEditor; using UnityEngine; using UnityEngine.TextCore; namespace Cielonos.Editor { /// /// 一键式 Editor 工具:将 Assets/Sprites/Icon/InputIcons/ 下的所有按键图标 /// 打包为 atlas 纹理,并自动生成 TMP_SpriteAsset 供 TMP 富文本使用。 /// 使用方式:Tools → Cielonos → Create InputGlyphs Sprite Asset /// public static class InputGlyphsSpriteAssetCreator { private const string KeyboardFolder = "Assets/Sprites/Icon/InputIcons/keyboard"; private const string MouseFolder = "Assets/Sprites/Icon/InputIcons/mouse"; private const string AtlasOutputFolder = "Assets/Scripts/SLSUtilities/UI/SpriteAssets"; private const string AssetOutputFolder = "Assets/TextMesh Pro/Resources/Sprite Assets"; private const string AtlasFileName = "InputGlyphs_Atlas.png"; private const string AssetFileName = "InputGlyphs.asset"; /// 打包前将每张源图缩放到此尺寸(像素),降低 atlas 体积。 private const int SpriteSize = 128; /// Atlas 最大边长。 private const int MaxAtlasSize = 2048; /// 打包时各图之间的像素间距。 private const int Padding = 2; /// 鼠标文件名 → Token 名映射表。 private static readonly Dictionary MouseNameMap = new() { { "mouse-left", "LMB" }, { "mouse-right", "RMB" }, { "mouse-middle", "MMB" }, { "mouse-g1", "Mouse4" }, { "mouse-g2", "Mouse5" }, { "mouse-move-hor", "MouseMoveH" }, { "mouse-move-vert", "MouseMoveV" }, }; private struct SpriteEntry { public string filePath; public string tokenName; } // ================================================================ // 入口 // ================================================================ [MenuItem("Tools/Cielonos/Create InputGlyphs Sprite Asset")] public static void Execute() { var entries = new List(); CollectEntries(KeyboardFolder, entries, MapKeyboardName); CollectEntries(MouseFolder, entries, MapMouseName); if (entries.Count == 0) { Debug.LogError("[InputGlyphs] InputIcons 文件夹中未找到任何图片。"); return; } Debug.Log($"[InputGlyphs] 发现 {entries.Count} 张图标,开始打包..."); // ── 1. 加载并缩放所有图标 ── LoadAndResizeResult loaded = LoadAndResize(entries); // ── 2. 打包为 atlas ── var atlas = new Texture2D(MaxAtlasSize, MaxAtlasSize, TextureFormat.RGBA32, false); Rect[] rects = atlas.PackTextures(loaded.Textures.ToArray(), Padding, MaxAtlasSize, false); if (rects == null) { Debug.LogError("[InputGlyphs] PackTextures 失败,请增大 MaxAtlasSize。"); CleanupTextures(loaded.Textures); return; } // ── 3. 保存 atlas PNG 到磁盘 ── string atlasPath = $"{AtlasOutputFolder}/{AtlasFileName}"; SaveAtlasToDisk(atlas, atlasPath); // ── 4. 配置 atlas 导入器,写入每个子 Sprite 的元数据 ── ConfigureAtlasImporter(atlasPath, atlas, rects, loaded.Names); // ── 5. 从已导入的 atlas 构建 TMP_SpriteAsset(放入 Resources 以便 TMP 按名查找) ── string assetPath = $"{AssetOutputFolder}/{AssetFileName}"; BuildSpriteAsset(atlasPath, assetPath); // ── 6. 清理临时纹理 ── CleanupTextures(loaded.Textures); UnityEngine.Object.DestroyImmediate(atlas); Debug.Log($"[InputGlyphs] SpriteAsset 已创建:{assetPath}({entries.Count} 个图标)。" + $"\n在 TMP 富文本中使用:"); } // ================================================================ // 收集文件 // ================================================================ private static void CollectEntries( string folder, List entries, Func nameMapper) { string[] guids = AssetDatabase.FindAssets("t:Texture2D", new[] { folder }); foreach (string guid in guids) { string assetPath = AssetDatabase.GUIDToAssetPath(guid); string fileName = Path.GetFileNameWithoutExtension(assetPath); entries.Add(new SpriteEntry { filePath = Path.GetFullPath(assetPath), tokenName = nameMapper(fileName) }); } } /// /// 键盘文件名 → Token 名。 /// 规则:单字母大写,单数字保留,Fn 键大写,连字符转 PascalCase,其余首字母大写。 /// private static string MapKeyboardName(string fileName) { // 单字母 → 大写 if (fileName.Length == 1 && char.IsLetter(fileName[0])) return fileName.ToUpperInvariant(); // 单数字 → 原样 if (fileName.Length == 1 && char.IsDigit(fileName[0])) return fileName; // 功能键 f1-f12 → 全大写 if (fileName.Length >= 2 && fileName[0] == 'f' && int.TryParse(fileName.Substring(1), out _)) return fileName.ToUpperInvariant(); // 含连字符 → PascalCase (arrow-down → ArrowDown) if (fileName.Contains('-')) { string[] parts = fileName.Split('-'); var sb = new StringBuilder(); foreach (string part in parts) { if (part.Length > 0) sb.Append(char.ToUpperInvariant(part[0])) .Append(part, 1, part.Length - 1); } return sb.ToString(); } // 其余 → 首字母大写 return char.ToUpperInvariant(fileName[0]) + fileName.Substring(1); } /// /// 鼠标文件名 → Token 名。通过静态映射表查找。 /// private static string MapMouseName(string fileName) { return MouseNameMap.TryGetValue(fileName, out string mapped) ? mapped : fileName; } // ================================================================ // 加载 & 缩放 // ================================================================ private struct LoadAndResizeResult { public List Textures; public List Names; } private static LoadAndResizeResult LoadAndResize(List entries) { var textures = new List(entries.Count); var names = new List(entries.Count); foreach (SpriteEntry entry in entries) { byte[] bytes = File.ReadAllBytes(entry.filePath); var src = new Texture2D(2, 2, TextureFormat.RGBA32, false); ImageConversion.LoadImage(src, bytes); Texture2D dst = Resize(src, SpriteSize, SpriteSize); UnityEngine.Object.DestroyImmediate(src); textures.Add(dst); names.Add(entry.tokenName); } return new LoadAndResizeResult { Textures = textures, Names = names }; } private static Texture2D Resize(Texture2D source, int width, int height) { RenderTexture rt = RenderTexture.GetTemporary(width, height, 0, RenderTextureFormat.ARGB32); rt.filterMode = FilterMode.Bilinear; Graphics.Blit(source, rt); RenderTexture prev = RenderTexture.active; RenderTexture.active = rt; var dst = new Texture2D(width, height, TextureFormat.RGBA32, false); dst.ReadPixels(new Rect(0, 0, width, height), 0, 0); dst.Apply(); RenderTexture.active = prev; RenderTexture.ReleaseTemporary(rt); return dst; } // ================================================================ // Atlas 保存 & 导入 // ================================================================ private static void SaveAtlasToDisk(Texture2D atlas, string atlasPath) { string dir = Path.GetDirectoryName(atlasPath); if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) Directory.CreateDirectory(dir); File.WriteAllBytes(atlasPath, atlas.EncodeToPNG()); AssetDatabase.ImportAsset(atlasPath, ImportAssetOptions.ForceUpdate); } private static void ConfigureAtlasImporter( string atlasPath, Texture2D atlas, Rect[] rects, List names) { var importer = AssetImporter.GetAtPath(atlasPath) as TextureImporter; if (importer == null) { Debug.LogError($"[InputGlyphs] 无法获取 TextureImporter:{atlasPath}"); return; } importer.textureType = TextureImporterType.Sprite; importer.spriteImportMode = SpriteImportMode.Multiple; importer.isReadable = false; importer.mipmapEnabled = false; importer.textureCompression = TextureImporterCompression.Uncompressed; importer.maxTextureSize = MaxAtlasSize; int atlasW = atlas.width; int atlasH = atlas.height; var spritesheet = new SpriteMetaData[rects.Length]; for (int i = 0; i < rects.Length; i++) { float x = rects[i].x * atlasW; float y = rects[i].y * atlasH; float w = rects[i].width * atlasW; float h = rects[i].height * atlasH; spritesheet[i] = new SpriteMetaData { name = names[i], rect = new Rect(x, y, w, h), alignment = (int)SpriteAlignment.Center, pivot = new Vector2(0.5f, 0.5f) }; } importer.spritesheet = spritesheet; importer.SaveAndReimport(); } // ================================================================ // TMP_SpriteAsset 构建 // ================================================================ private static void BuildSpriteAsset(string atlasPath, string assetPath) { // 加载持久化后的 atlas 纹理 Texture2D atlasTexture = AssetDatabase.LoadAssetAtPath(atlasPath); if (atlasTexture == null) { Debug.LogError($"[InputGlyphs] 无法加载 atlas:{atlasPath}"); return; } // 从 atlas 加载所有子 Sprite Sprite[] sprites = AssetDatabase.LoadAllAssetsAtPath(atlasPath) .OfType() .OrderByDescending(s => s.rect.y) .ThenBy(s => s.rect.x) .ToArray(); if (sprites.Length == 0) { Debug.LogError("[InputGlyphs] Atlas 中未找到子 Sprite,请检查导入设置。"); return; } // 创建 TMP_SpriteAsset 实例 TMP_SpriteAsset spriteAsset = ScriptableObject.CreateInstance(); string dir = Path.GetDirectoryName(assetPath); if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) Directory.CreateDirectory(dir); // 如果已存在旧资产则删除 if (AssetDatabase.LoadAssetAtPath(assetPath) != null) AssetDatabase.DeleteAsset(assetPath); AssetDatabase.CreateAsset(spriteAsset, assetPath); // 设置 sprite sheet 引用 spriteAsset.spriteSheet = atlasTexture; // 通过反射设置版本号(internal setter) SetFieldValue(spriteAsset, "m_Version", "1.1.0"); // 设置 hash code spriteAsset.hashCode = TMP_TextUtilities.GetSimpleHashCode(spriteAsset.name); // 构建 Glyph 和 Character 表 var glyphTable = new List(sprites.Length); var charTable = new List(sprites.Length); for (int i = 0; i < sprites.Length; i++) { Sprite sprite = sprites[i]; var glyph = new TMP_SpriteGlyph( index: (uint)i, metrics: new GlyphMetrics( sprite.rect.width, sprite.rect.height, 0f, sprite.rect.height * 0.8f, sprite.rect.width), glyphRect: new GlyphRect(sprite.rect), scale: 1.0f, atlasIndex: 0, sprite: sprite ); glyphTable.Add(glyph); var character = new TMP_SpriteCharacter(0xFFFE, glyph) { name = sprite.name, scale = 1.0f }; charTable.Add(character); } // 通过反射设置内部字段(setter 为 internal) SetFieldValue(spriteAsset, "m_GlyphTable", glyphTable); SetFieldValue(spriteAsset, "m_SpriteCharacterTable", charTable); // 创建并嵌入 Material Shader shader = Shader.Find("TextMeshPro/Sprite"); if (shader == null) { Debug.LogWarning("[InputGlyphs] Shader 'TextMeshPro/Sprite' 未找到。"); return; } var material = new Material(shader); material.SetTexture(ShaderUtilities.ID_MainTex, atlasTexture); material.name = spriteAsset.name + " Material"; spriteAsset.material = material; AssetDatabase.AddObjectToAsset(material, spriteAsset); // 重建查找表并保存 spriteAsset.UpdateLookupTables(); EditorUtility.SetDirty(spriteAsset); AssetDatabase.SaveAssets(); AssetDatabase.ImportAsset(assetPath); } // ================================================================ // 工具方法 // ================================================================ private static void SetFieldValue(object target, string fieldName, object value) { FieldInfo field = target.GetType().GetField( fieldName, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public); if (field != null) { field.SetValue(target, value); } else { Debug.LogWarning($"[InputGlyphs] 反射未找到字段 '{fieldName}'(类型:{target.GetType().Name})。"); } } private static void CleanupTextures(List textures) { foreach (Texture2D tex in textures) { if (tex != null) UnityEngine.Object.DestroyImmediate(tex); } } } }