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;
}
}