using UnityEngine; using UnityEditor; using UnityEditor.U2D; using UnityEngine.U2D; using System.IO; using System.Collections.Generic; using System.Linq; using System.Text; using ArtResource; using Thrift; using Thrift.Protocol; using Thrift.Transport; using Thrift.Transport.Client; namespace ArtTools { /// /// 图集构建工具 /// public class AtlasBuilderEditor : EditorWindow { private const string ATLAS_ROOT_PATH = "Assets/Art_SubModule/Art_Atlas"; private const string SPRITE_ROOT_PATH = "Assets/Art_SubModule"; private const string CONFIG_PATH = "Assets/Art_SubModule/Art_Atlas/AtlasConfig.json"; private const string BYTES_PATH = "Assets/Art_SubModule/Art_Bytes/ArtAtlasConfig.bytes"; private enum MenuState { Normal, Add, Rename, Remove } // 数据 private AtlasConfig config; private AtlasData selectedAtlas; private SpriteFolder spriteRoot; // UI状态 private MenuState menuState = MenuState.Normal; private Vector2 atlasListScroll = Vector2.zero; private Vector2 atlasContentScroll = Vector2.zero; private Vector2 spriteListScroll = Vector2.zero; // 选择状态 private HashSet selectedSpritesInAtlas = new HashSet(); private HashSet expandedSpriteFolders = new HashSet(); private HashSet selectedSpriteAssets = new HashSet(); private HashSet selectedSpriteFolders = new HashSet(); // 缓存 private HashSet cachedAssignedSprites = new HashSet(); private HashSet cachedAssignedFolders = new HashSet(); private Dictionary savedAtlasSignatures = new Dictionary(); // 输入 private string inputAtlasName = ""; private bool hideAssignedSprites = false; // 绘制计数 private int currentAtlasRowOnDraw = 0; private int currentSpriteRowOnDraw = 0; [MenuItem("程序工具/图集构建工具")] public static void ShowWindow() { var window = GetWindow("图集构建工具"); window.minSize = new Vector2(1400, 600); window.Show(); } private void OnEnable() { LoadConfig(); RefreshSpriteTree(); } private void OnGUI() { EditorGUILayout.BeginHorizontal(GUILayout.Width(position.width), GUILayout.Height(position.height)); { GUILayout.Space(2f); // 左侧:图集列表 EditorGUILayout.BeginVertical(GUILayout.Width(position.width * 0.25f)); { GUILayout.Space(5f); EditorGUILayout.LabelField($"图集列表 ({config.Atlases.Count})", EditorStyles.boldLabel); EditorGUILayout.BeginHorizontal("box", GUILayout.Height(position.height - 52f)); { DrawAtlasList(); } EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); { GUILayout.Space(5f); DrawAtlasListMenu(); } EditorGUILayout.EndHorizontal(); } EditorGUILayout.EndVertical(); // 中间:图集内容 EditorGUILayout.BeginVertical(GUILayout.Width(position.width * 0.25f)); { GUILayout.Space(5f); EditorGUILayout.LabelField($"图集内容 ({(selectedAtlas != null ? selectedAtlas.SpritePaths.Count : 0)})", EditorStyles.boldLabel); EditorGUILayout.BeginHorizontal("box", GUILayout.Height(position.height - 52f)); { DrawAtlasContent(); } EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); { GUILayout.Space(5f); DrawAtlasContentMenu(); } EditorGUILayout.EndHorizontal(); } EditorGUILayout.EndVertical(); // 右侧:Sprite列表 EditorGUILayout.BeginVertical(GUILayout.Width(position.width * 0.5f - 16f)); { GUILayout.Space(5f); EditorGUILayout.LabelField("Sprite列表", EditorStyles.boldLabel); EditorGUILayout.BeginHorizontal("box", GUILayout.Height(position.height - 52f)); { DrawSpriteList(); } EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); { GUILayout.Space(5f); DrawSpriteListMenu(); } EditorGUILayout.EndHorizontal(); } EditorGUILayout.EndVertical(); GUILayout.Space(5f); } EditorGUILayout.EndHorizontal(); } // ==================== 绘制方法 ==================== private void DrawAtlasList() { currentAtlasRowOnDraw = 0; atlasListScroll = EditorGUILayout.BeginScrollView(atlasListScroll); { if (config != null && config.Atlases != null) { foreach (var atlas in config.Atlases) { DrawAtlasItem(atlas); } } } EditorGUILayout.EndScrollView(); } private void DrawAtlasItem(AtlasData atlas) { EditorGUILayout.BeginHorizontal(); { bool isSelected = selectedAtlas == atlas; float emptySpace = position.width * 0.25f; if (EditorGUILayout.Toggle(isSelected, GUILayout.Width(emptySpace - 12f))) { ChangeSelectedAtlas(atlas); } else if (isSelected) { ChangeSelectedAtlas(null); } GUILayout.Space(-emptySpace + 24f); Texture2D icon = EditorGUIUtility.FindTexture("SpriteAtlas Icon"); GUI.DrawTexture(new Rect(32f, 20f * currentAtlasRowOnDraw + 3f, 16f, 16f), icon); GUILayout.Space(20f); EditorGUILayout.LabelField($"{atlas.Name} ({atlas.SpritePaths.Count})"); } EditorGUILayout.EndHorizontal(); currentAtlasRowOnDraw++; } private void DrawAtlasListMenu() { switch (menuState) { case MenuState.Normal: DrawAtlasListMenu_Normal(); break; case MenuState.Add: DrawAtlasListMenu_Add(); break; case MenuState.Rename: DrawAtlasListMenu_Rename(); break; case MenuState.Remove: DrawAtlasListMenu_Remove(); break; } } private void DrawAtlasListMenu_Normal() { if (GUILayout.Button("添加", GUILayout.Width(65f))) { menuState = MenuState.Add; inputAtlasName = ""; GUI.FocusControl(null); } EditorGUI.BeginDisabledGroup(selectedAtlas == null); { if (GUILayout.Button("重命名", GUILayout.Width(65f))) { menuState = MenuState.Rename; inputAtlasName = selectedAtlas != null ? selectedAtlas.Name : ""; GUI.FocusControl(null); } if (GUILayout.Button("移除", GUILayout.Width(65f))) { menuState = MenuState.Remove; } } EditorGUI.EndDisabledGroup(); } private void DrawAtlasListMenu_Add() { GUI.SetNextControlName("NewAtlasNameTextField"); inputAtlasName = EditorGUILayout.TextField(inputAtlasName); if (GUI.GetNameOfFocusedControl() == "NewAtlasNameTextField") { if (Event.current.isKey && Event.current.keyCode == KeyCode.Return) { AddAtlas(inputAtlasName); Repaint(); } } if (GUILayout.Button("添加", GUILayout.Width(50f))) { AddAtlas(inputAtlasName); } if (GUILayout.Button("取消", GUILayout.Width(50f))) { menuState = MenuState.Normal; } } private void DrawAtlasListMenu_Rename() { if (selectedAtlas == null) { menuState = MenuState.Normal; return; } GUI.SetNextControlName("RenameAtlasNameTextField"); inputAtlasName = EditorGUILayout.TextField(inputAtlasName); if (GUI.GetNameOfFocusedControl() == "RenameAtlasNameTextField") { if (Event.current.isKey && Event.current.keyCode == KeyCode.Return) { RenameAtlas(selectedAtlas, inputAtlasName); Repaint(); } } if (GUILayout.Button("确定", GUILayout.Width(50f))) { RenameAtlas(selectedAtlas, inputAtlasName); } if (GUILayout.Button("取消", GUILayout.Width(50f))) { menuState = MenuState.Normal; } } private void DrawAtlasListMenu_Remove() { if (selectedAtlas == null) { menuState = MenuState.Normal; return; } GUILayout.Label($"移除图集 '{selectedAtlas.Name}' ?"); if (GUILayout.Button("确定", GUILayout.Width(50f))) { RemoveAtlas(); menuState = MenuState.Normal; } if (GUILayout.Button("取消", GUILayout.Width(50f))) { menuState = MenuState.Normal; } } private void DrawAtlasContent() { atlasContentScroll = EditorGUILayout.BeginScrollView(atlasContentScroll); { if (selectedAtlas != null) { foreach (string spritePath in selectedAtlas.SpritePaths) { EditorGUILayout.BeginHorizontal(); { bool select = selectedSpritesInAtlas.Contains(spritePath); float emptySpace = position.width * 0.25f; if (select != EditorGUILayout.Toggle(select, GUILayout.Width(emptySpace - 12f))) { if (select) selectedSpritesInAtlas.Remove(spritePath); else selectedSpritesInAtlas.Add(spritePath); } GUILayout.Space(-emptySpace + 24f); Sprite sprite = AssetDatabase.LoadAssetAtPath(spritePath); // 正确处理图集sprite的预览 Rect iconRect = new Rect(32f, 20f * (selectedAtlas.SpritePaths.IndexOf(spritePath)) + 3f, 16f, 16f); if (sprite != null && sprite.texture != null) { Rect texCoords = sprite.textureRect; Texture2D tex = sprite.texture; Rect normalizedCoords = new Rect( texCoords.x / tex.width, texCoords.y / tex.height, texCoords.width / tex.width, texCoords.height / tex.height ); GUI.DrawTextureWithTexCoords(iconRect, tex, normalizedCoords, true); } else { Texture2D icon = EditorGUIUtility.FindTexture("Sprite Icon"); GUI.DrawTexture(iconRect, icon); } EditorGUILayout.LabelField(string.Empty, GUILayout.Width(26f), GUILayout.Height(18f)); EditorGUILayout.LabelField(sprite != null ? sprite.name : Path.GetFileName(spritePath)); } EditorGUILayout.EndHorizontal(); } } } EditorGUILayout.EndScrollView(); } private void DrawAtlasContentMenu() { if (GUILayout.Button("全选", GUILayout.Width(50f)) && selectedAtlas != null) { selectedSpritesInAtlas.Clear(); foreach (string path in selectedAtlas.SpritePaths) { selectedSpritesInAtlas.Add(path); } } if (GUILayout.Button("全不选", GUILayout.Width(60f))) { selectedSpritesInAtlas.Clear(); } GUILayout.Label(string.Empty); EditorGUI.BeginDisabledGroup(selectedAtlas == null || selectedSpritesInAtlas.Count <= 0); { if (GUILayout.Button($"{selectedSpritesInAtlas.Count} >>", GUILayout.Width(80f))) { foreach (string path in selectedSpritesInAtlas.ToList()) { selectedAtlas.SpritePaths.Remove(path); } selectedSpritesInAtlas.Clear(); RefreshAssignedCache(); } } EditorGUI.EndDisabledGroup(); } private void DrawSpriteList() { currentSpriteRowOnDraw = 0; spriteListScroll = EditorGUILayout.BeginScrollView(spriteListScroll); { if (spriteRoot != null) { DrawSpriteFolder(spriteRoot); } } EditorGUILayout.EndScrollView(); } private void DrawSpriteFolder(SpriteFolder folder) { // 跳过没有可见Sprite的文件夹(HasVisibleSprites已经考虑了hideAssignedSprites) if (!HasVisibleSprites(folder)) { return; } EditorGUILayout.BeginHorizontal(); { bool select = IsSelectedFolder(folder); if (select != EditorGUILayout.Toggle(select, GUILayout.Width(12f + 14f * folder.Depth))) { SetSelectedFolder(folder, !select); } GUILayout.Space(-14f * folder.Depth); bool expand = expandedSpriteFolders.Contains(folder); bool foldout = EditorGUI.Foldout(new Rect(18f + 14f * folder.Depth, 20f * currentSpriteRowOnDraw + 4f, int.MaxValue, 14f), expand, string.Empty, true); if (expand != foldout) { if (foldout) expandedSpriteFolders.Add(folder); else expandedSpriteFolders.Remove(folder); } Texture2D icon = EditorGUIUtility.FindTexture("Folder Icon"); GUI.DrawTexture(new Rect(32f + 14f * folder.Depth, 20f * currentSpriteRowOnDraw + 3f, 16f, 16f), icon); EditorGUILayout.LabelField(string.Empty, GUILayout.Width(30f + 14f * folder.Depth), GUILayout.Height(18f)); EditorGUILayout.LabelField(folder.Name); } EditorGUILayout.EndHorizontal(); currentSpriteRowOnDraw++; if (expandedSpriteFolders.Contains(folder)) { foreach (var subFolder in folder.SubFolders) { DrawSpriteFolder(subFolder); } foreach (var sprite in folder.Sprites) { DrawSpriteAsset(sprite); } } } private void DrawSpriteAsset(SpriteAsset spriteAsset) { if (hideAssignedSprites && IsAssignedSprite(spriteAsset)) { return; } EditorGUILayout.BeginHorizontal(); { bool select = selectedSpriteAssets.Contains(spriteAsset); float emptySpace = position.width * 0.5f - 16f; if (select != EditorGUILayout.Toggle(select, GUILayout.Width(emptySpace - 12f))) { SetSelectedSprite(spriteAsset, !select); } GUILayout.Space(-emptySpace + 24f); Sprite sprite = AssetDatabase.LoadAssetAtPath(spriteAsset.Path); // 正确处理图集sprite的预览 Rect iconRect = new Rect(32f + 14f * spriteAsset.Depth, 20f * currentSpriteRowOnDraw + 3f, 16f, 16f); if (sprite != null && sprite.texture != null) { Rect texCoords = sprite.textureRect; Texture2D tex = sprite.texture; Rect normalizedCoords = new Rect( texCoords.x / tex.width, texCoords.y / tex.height, texCoords.width / tex.width, texCoords.height / tex.height ); GUI.DrawTextureWithTexCoords(iconRect, tex, normalizedCoords, true); } else { Texture2D icon = EditorGUIUtility.FindTexture("Sprite Icon"); GUI.DrawTexture(iconRect, icon); } EditorGUILayout.LabelField(string.Empty, GUILayout.Width(26f + 14f * spriteAsset.Depth), GUILayout.Height(18f)); EditorGUILayout.LabelField(spriteAsset.Name); } EditorGUILayout.EndHorizontal(); currentSpriteRowOnDraw++; } private void DrawSpriteListMenu() { HashSet selectedSprites = GetSelectedSprites(); EditorGUI.BeginDisabledGroup(selectedAtlas == null || selectedSprites.Count <= 0); { if (GUILayout.Button($"<< {selectedSprites.Count}", GUILayout.Width(80f))) { foreach (var sprite in selectedSprites) { if (!selectedAtlas.SpritePaths.Contains(sprite.Path)) { selectedAtlas.SpritePaths.Add(sprite.Path); } } selectedSpriteAssets.Clear(); selectedSpriteFolders.Clear(); RefreshAssignedCache(); } } EditorGUI.EndDisabledGroup(); bool newHideState = EditorGUILayout.ToggleLeft("隐藏在图集中的sprite", hideAssignedSprites, GUILayout.Width(180f)); if (newHideState != hideAssignedSprites) { hideAssignedSprites = newHideState; RefreshAssignedCache(); } GUILayout.Label(string.Empty); if (GUILayout.Button("清除丢失引用", GUILayout.Width(100f))) { CleanMissingReferences(); } if (GUILayout.Button("保存", GUILayout.Width(80f))) { SaveAndBuildAllAtlases(); } if (GUILayout.Button("全部重打包", GUILayout.Width(90f))) { SaveAndBuildAllAtlases(true); } } // ==================== 逻辑方法 ==================== private void ChangeSelectedAtlas(AtlasData atlas) { if (selectedAtlas == atlas) return; selectedAtlas = atlas; selectedSpritesInAtlas.Clear(); RefreshAssignedCache(); } private void AddAtlas(string name) { if (string.IsNullOrWhiteSpace(name)) { EditorUtility.DisplayDialog("错误", "图集名称不能为空", "确定"); return; } if (config.Atlases.Any(a => a.Name == name)) { EditorUtility.DisplayDialog("错误", $"图集 '{name}' 已存在", "确定"); return; } config.Atlases.Add(new AtlasData { Name = name, SpritePaths = new List() }); menuState = MenuState.Normal; selectedAtlas = config.Atlases[config.Atlases.Count - 1]; } private void RenameAtlas(AtlasData atlas, string newName) { if (string.IsNullOrWhiteSpace(newName)) { EditorUtility.DisplayDialog("错误", "图集名称不能为空", "确定"); return; } if (config.Atlases.Any(a => a != atlas && a.Name == newName)) { EditorUtility.DisplayDialog("错误", $"图集 '{newName}' 已存在", "确定"); return; } string oldPath = $"{ATLAS_ROOT_PATH}/{atlas.Name}.spriteatlasv2"; string newPath = $"{ATLAS_ROOT_PATH}/{newName}.spriteatlasv2"; if (File.Exists(oldPath)) { AssetDatabase.MoveAsset(oldPath, newPath); } atlas.Name = newName; menuState = MenuState.Normal; } private void RemoveAtlas() { if (selectedAtlas == null) return; string atlasPath = $"{ATLAS_ROOT_PATH}/{selectedAtlas.Name}.spriteatlasv2"; if (File.Exists(atlasPath)) { AssetDatabase.DeleteAsset(atlasPath); } config.Atlases.Remove(selectedAtlas); selectedAtlas = null; selectedSpritesInAtlas.Clear(); } private void CleanMissingReferences() { int cleanedCount = 0; foreach (var atlas in config.Atlases) { int originalCount = atlas.SpritePaths.Count; atlas.SpritePaths.RemoveAll(path => !File.Exists(path)); cleanedCount += originalCount - atlas.SpritePaths.Count; } if (cleanedCount > 0) { RefreshAssignedCache(); EditorUtility.DisplayDialog("提示", $"已清除 {cleanedCount} 个丢失的引用", "确定"); } else { EditorUtility.DisplayDialog("提示", "没有发现丢失的引用", "确定"); } } private void SaveAndBuildAllAtlases(bool forceRebuild = false) { var timer = System.Diagnostics.Stopwatch.StartNew(); int importedAtlasCount = 0; List atlasesToPack = new List(); try { SaveConfig(); CleanOldSpriteAtlasFiles(); if (!Directory.Exists(ATLAS_ROOT_PATH)) { Directory.CreateDirectory(ATLAS_ROOT_PATH); } int count = config.Atlases.Count; for (int i = 0; i < count; i++) { var atlas = config.Atlases[i]; float progress = count > 0 ? (float)i / count * 0.7f : 0.7f; EditorUtility.DisplayProgressBar("构建图集", $"检查 {atlas.Name}...", progress); string atlasPath = $"{ATLAS_ROOT_PATH}/{atlas.Name}.spriteatlasv2"; string atlasSignature = GetAtlasSignature(atlas); bool atlasDataChanged = forceRebuild || !savedAtlasSignatures.TryGetValue(atlas.Name, out string savedSignature) || savedSignature != atlasSignature; bool atlasFileMissing = !File.Exists(atlasPath); if (!atlasDataChanged && !atlasFileMissing) { continue; } bool atlasFileChanged = WriteV2AtlasFile(atlas); SpriteAtlas spriteAtlas = null; bool shouldImportAtlas = atlasFileChanged || atlasFileMissing; if (shouldImportAtlas) { AssetDatabase.ImportAsset(atlasPath, ImportAssetOptions.ForceSynchronousImport | ImportAssetOptions.ForceUpdate); importedAtlasCount++; } spriteAtlas = AssetDatabase.LoadAssetAtPath(atlasPath); if (spriteAtlas == null && File.Exists(atlasPath) && !shouldImportAtlas) { AssetDatabase.ImportAsset(atlasPath, ImportAssetOptions.ForceSynchronousImport | ImportAssetOptions.ForceUpdate); importedAtlasCount++; spriteAtlas = AssetDatabase.LoadAssetAtPath(atlasPath); } if (spriteAtlas == null) { Debug.LogWarning($"[AtlasBuilderEditor] 图集加载失败,跳过打包: {atlasPath}"); continue; } savedAtlasSignatures[atlas.Name] = atlasSignature; if ((forceRebuild || atlasDataChanged || atlasFileChanged || atlasFileMissing) && atlas.SpritePaths.Count > 0) { atlasesToPack.Add(spriteAtlas); } } if (atlasesToPack.Count > 0) { EditorUtility.DisplayProgressBar("构建图集", $"打包 {atlasesToPack.Count} 个图集...", 0.85f); SpriteAtlasUtility.PackAtlases(atlasesToPack.ToArray(), EditorUserBuildSettings.activeBuildTarget, true); } AssetDatabase.SaveAssets(); timer.Stop(); string modeText = forceRebuild ? "全部重打包" : "增量保存"; EditorUtility.DisplayDialog("提示", $"{modeText}完成\n导入图集: {importedAtlasCount}\n打包图集: {atlasesToPack.Count}\n耗时: {timer.Elapsed.TotalSeconds:F1} 秒", "确定"); } finally { EditorUtility.ClearProgressBar(); } } /// /// 直接写入 .spriteatlasv2 YAML 文件(V2 格式) /// private bool WriteV2AtlasFile(AtlasData atlasData) { string atlasPath = $"{ATLAS_ROOT_PATH}/{atlasData.Name}.spriteatlasv2"; // 收集所有有效 Sprite 的 GUID 和 localFileID var packableEntries = new List(); foreach (string spritePath in atlasData.SpritePaths) { Sprite sprite = AssetDatabase.LoadAssetAtPath(spritePath); if (sprite != null) { string guid; long localId; if (AssetDatabase.TryGetGUIDAndLocalFileIdentifier(sprite, out guid, out localId)) { packableEntries.Add($" - {{fileID: {localId}, guid: {guid}, type: 3}}"); } } } // 构建 YAML var sb = new StringBuilder(); sb.Append("%YAML 1.1\n"); sb.Append("%TAG !u! tag:unity3d.com,2011:\n"); sb.Append("--- !u!612988286 &1\n"); sb.Append("SpriteAtlasAsset:\n"); sb.Append(" m_ObjectHideFlags: 0\n"); sb.Append(" m_CorrespondingSourceObject: {fileID: 0}\n"); sb.Append(" m_PrefabInstance: {fileID: 0}\n"); sb.Append(" m_PrefabAsset: {fileID: 0}\n"); sb.Append(" m_Name:\n"); sb.Append(" serializedVersion: 2\n"); sb.Append(" m_MasterAtlas: {fileID: 0}\n"); sb.Append(" m_ImporterData:\n"); if (packableEntries.Count > 0) { sb.Append(" packables:\n"); foreach (string entry in packableEntries) { sb.Append(entry).Append("\n"); } } else { sb.Append(" packables: []\n"); } sb.Append(" m_IsVariant: 0\n"); return WriteTextIfChanged(atlasPath, sb.ToString()); } /// /// 清理旧版 .spriteatlas 文件(V1格式),只保留 .spriteatlasv2 /// private void CleanOldSpriteAtlasFiles() { if (!Directory.Exists(ATLAS_ROOT_PATH)) return; string[] oldFiles = Directory.GetFiles(ATLAS_ROOT_PATH, "*.spriteatlas") .Where(f => !f.EndsWith(".spriteatlasv2") && !f.EndsWith(".meta")) .ToArray(); foreach (string oldFile in oldFiles) { string assetPath = oldFile.Replace("\\", "/"); AssetDatabase.DeleteAsset(assetPath); } if (oldFiles.Length > 0) { Debug.Log($"[AtlasBuilderEditor] 已清理 {oldFiles.Length} 个旧版 .spriteatlas 文件"); } } private void BuildAtlas(AtlasData atlasData) { if (!Directory.Exists(ATLAS_ROOT_PATH)) { Directory.CreateDirectory(ATLAS_ROOT_PATH); } string atlasPath = $"{ATLAS_ROOT_PATH}/{atlasData.Name}.spriteatlasv2"; bool atlasFileChanged = WriteV2AtlasFile(atlasData); SpriteAtlas spriteAtlas = AssetDatabase.LoadAssetAtPath(atlasPath); if (atlasFileChanged || spriteAtlas == null) { AssetDatabase.ImportAsset(atlasPath, ImportAssetOptions.ForceSynchronousImport | ImportAssetOptions.ForceUpdate); spriteAtlas = AssetDatabase.LoadAssetAtPath(atlasPath); } if (spriteAtlas != null && atlasData.SpritePaths.Count > 0) { SpriteAtlasUtility.PackAtlases(new[] { spriteAtlas }, EditorUserBuildSettings.activeBuildTarget, true); } } private void LoadConfig() { if (File.Exists(CONFIG_PATH)) { string json = File.ReadAllText(CONFIG_PATH); config = JsonUtility.FromJson(json); } else { config = new AtlasConfig { Atlases = new List() }; SaveConfig(); } RefreshSavedAtlasSignatures(); } private void RefreshSavedAtlasSignatures() { savedAtlasSignatures.Clear(); if (config == null || config.Atlases == null) { return; } foreach (var atlas in config.Atlases) { savedAtlasSignatures[atlas.Name] = GetAtlasSignature(atlas); } } private string GetAtlasSignature(AtlasData atlas) { if (atlas == null || atlas.SpritePaths == null || atlas.SpritePaths.Count == 0) { return string.Empty; } return string.Join("\n", atlas.SpritePaths); } private bool SaveConfig() { bool changed = false; string dir = Path.GetDirectoryName(CONFIG_PATH); if (!Directory.Exists(dir)) { Directory.CreateDirectory(dir); } string json = JsonUtility.ToJson(config, true); bool jsonChanged = WriteTextIfChanged(CONFIG_PATH, json); if (jsonChanged) { AssetDatabase.ImportAsset(CONFIG_PATH, ImportAssetOptions.ForceUpdate); changed = true; } if ((jsonChanged || !File.Exists(BYTES_PATH)) && SaveConfigToThriftBytes()) { AssetDatabase.ImportAsset(BYTES_PATH, ImportAssetOptions.ForceUpdate); changed = true; } return changed; } /// /// 保存配置到Thrift Bytes格式 /// private bool SaveConfigToThriftBytes() { try { var startTime = System.Diagnostics.Stopwatch.StartNew(); // 确保输出目录存在 string dir = Path.GetDirectoryName(BYTES_PATH); if (!Directory.Exists(dir)) { Directory.CreateDirectory(dir); } // 创建Thrift数据结构 var thriftConfig = new Byway.Thrift.Data.ArtAtlasConfig { Atlases = new List(), Version = System.DateTimeOffset.UtcNow.ToUnixTimeSeconds(), GenerateTime = System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") }; // 转换所有Atlas foreach (var atlas in config.Atlases) { var thriftAtlas = new Byway.Thrift.Data.ArtAtlasInfo { Name = atlas.Name, SpritePaths = new List(atlas.SpritePaths) }; thriftConfig.Atlases.Add(thriftAtlas); } // 序列化到bytes byte[] bytesData; using (var memoryStream = new MemoryStream()) { using (var transport = new TStreamTransport(null, memoryStream, new TConfiguration())) { using (var protocol = new TBinaryProtocol(transport)) { thriftConfig.WriteAsync(protocol, System.Threading.CancellationToken.None).GetAwaiter().GetResult(); } } bytesData = memoryStream.ToArray(); } bool changed = WriteBytesIfChanged(BYTES_PATH, bytesData); startTime.Stop(); if (changed) { Debug.Log($"[AtlasBuilderEditor] Atlas Bytes生成成功!\n" + $" 路径: {BYTES_PATH}\n" + $" 图集数量: {thriftConfig.Atlases.Count}\n" + $" 文件大小: {bytesData.Length / 1024f:F2} KB\n" + $" 耗时: {startTime.ElapsedMilliseconds} ms"); } return changed; } catch (System.Exception ex) { Debug.LogError($"[AtlasBuilderEditor] 生成Atlas Bytes失败: {ex.Message}\n{ex.StackTrace}"); return false; } } private bool WriteTextIfChanged(string path, string content) { if (File.Exists(path) && File.ReadAllText(path) == content) { return false; } string dir = Path.GetDirectoryName(path); if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) { Directory.CreateDirectory(dir); } File.WriteAllText(path, content); return true; } private bool WriteBytesIfChanged(string path, byte[] content) { if (File.Exists(path)) { byte[] current = File.ReadAllBytes(path); if (current.SequenceEqual(content)) { return false; } } string dir = Path.GetDirectoryName(path); if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) { Directory.CreateDirectory(dir); } File.WriteAllBytes(path, content); return true; } private void RefreshSpriteTree() { spriteRoot = new SpriteFolder("Art_SubModule", null, 0); BuildSpriteTree(SPRITE_ROOT_PATH, spriteRoot); RefreshAssignedCache(); } private void BuildSpriteTree(string path, SpriteFolder parent) { if (!Directory.Exists(path)) return; // 添加子文件夹 foreach (string dir in Directory.GetDirectories(path)) { string folderName = Path.GetFileName(dir); var subFolder = new SpriteFolder(folderName, parent, parent.Depth + 1); parent.SubFolders.Add(subFolder); BuildSpriteTree(dir, subFolder); } // 添加Sprite文件 var guids = AssetDatabase.FindAssets("t:Sprite", new[] { path }); var spritePaths = guids.Select(guid => AssetDatabase.GUIDToAssetPath(guid)) .Where(p => Path.GetDirectoryName(p).Replace("\\", "/") == path.Replace("\\", "/")) .OrderBy(p => p); foreach (string spritePath in spritePaths) { Sprite sprite = AssetDatabase.LoadAssetAtPath(spritePath); if (sprite != null) { parent.Sprites.Add(new SpriteAsset { Name = sprite.name, Path = spritePath, Depth = parent.Depth + 1, Parent = parent }); } } } private void RefreshAssignedCache() { cachedAssignedSprites.Clear(); cachedAssignedFolders.Clear(); HashSet allAssignedPaths = new HashSet(); foreach (var atlas in config.Atlases) { foreach (string path in atlas.SpritePaths) { allAssignedPaths.Add(path); } } MarkAssignedRecursive(spriteRoot, allAssignedPaths); } private bool MarkAssignedRecursive(SpriteFolder folder, HashSet assignedPaths) { bool hasAssigned = false; foreach (var subFolder in folder.SubFolders) { if (MarkAssignedRecursive(subFolder, assignedPaths)) { hasAssigned = true; } } foreach (var sprite in folder.Sprites) { if (assignedPaths.Contains(sprite.Path)) { cachedAssignedSprites.Add(sprite); hasAssigned = true; } } if (hasAssigned) { cachedAssignedFolders.Add(folder); } return hasAssigned; } // ==================== 辅助方法 ==================== private bool IsSelectedFolder(SpriteFolder folder) { foreach (var subFolder in folder.SubFolders) { if (!IsSelectedFolder(subFolder)) return false; } foreach (var sprite in folder.Sprites) { if (!selectedSpriteAssets.Contains(sprite)) return false; } return folder.SubFolders.Count > 0 || folder.Sprites.Count > 0; } private void SetSelectedFolder(SpriteFolder folder, bool select) { if (select) { selectedSpriteFolders.Add(folder); foreach (var sprite in folder.Sprites) { selectedSpriteAssets.Add(sprite); } } else { selectedSpriteFolders.Remove(folder); foreach (var sprite in folder.Sprites) { selectedSpriteAssets.Remove(sprite); } } foreach (var subFolder in folder.SubFolders) { SetSelectedFolder(subFolder, select); } } private void SetSelectedSprite(SpriteAsset sprite, bool select) { if (select) { selectedSpriteAssets.Add(sprite); } else { selectedSpriteAssets.Remove(sprite); } } private bool IsAssignedSprite(SpriteAsset sprite) { return cachedAssignedSprites.Contains(sprite); } private bool IsAssignedFolder(SpriteFolder folder) { return cachedAssignedFolders.Contains(folder); } private HashSet GetSelectedSprites() { if (!hideAssignedSprites) { return selectedSpriteAssets; } HashSet result = new HashSet(); foreach (var sprite in selectedSpriteAssets) { if (!IsAssignedSprite(sprite)) { result.Add(sprite); } } return result; } private bool HasVisibleSprites(SpriteFolder folder) { // 检查当前文件夹的Sprite foreach (var sprite in folder.Sprites) { if (!hideAssignedSprites || !IsAssignedSprite(sprite)) { return true; } } // 递归检查子文件夹 foreach (var subFolder in folder.SubFolders) { if (HasVisibleSprites(subFolder)) return true; } return false; } } // ==================== 数据类 ==================== public class SpriteFolder { public string Name; public SpriteFolder Parent; public int Depth; public List SubFolders = new List(); public List Sprites = new List(); public SpriteFolder(string name, SpriteFolder parent, int depth) { Name = name; Parent = parent; Depth = depth; } } public class SpriteAsset { public string Name; public string Path; public int Depth; public SpriteFolder Parent; } }