Art_SubModule/Art_Scripts/Editor/AtlasBuilderEditor.cs
2026-04-07 10:16:55 +08:00

1130 lines
40 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.

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
{
/// <summary>
/// 图集构建工具
/// </summary>
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<string> selectedSpritesInAtlas = new HashSet<string>();
private HashSet<SpriteFolder> expandedSpriteFolders = new HashSet<SpriteFolder>();
private HashSet<SpriteAsset> selectedSpriteAssets = new HashSet<SpriteAsset>();
private HashSet<SpriteFolder> selectedSpriteFolders = new HashSet<SpriteFolder>();
// 缓存
private HashSet<SpriteAsset> cachedAssignedSprites = new HashSet<SpriteAsset>();
private HashSet<SpriteFolder> cachedAssignedFolders = new HashSet<SpriteFolder>();
// 输入
private string inputAtlasName = "";
private bool hideAssignedSprites = false;
// 绘制计数
private int currentAtlasRowOnDraw = 0;
private int currentSpriteRowOnDraw = 0;
[MenuItem("程序工具/图集构建工具")]
public static void ShowWindow()
{
var window = GetWindow<AtlasBuilderEditor>("图集构建工具");
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<Sprite>(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<Sprite>(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<SpriteAsset> 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();
}
}
// ==================== 逻辑方法 ====================
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<string>()
});
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()
{
SaveConfig();
// 清理旧版 .spriteatlas 文件
CleanOldSpriteAtlasFiles();
if (!Directory.Exists(ATLAS_ROOT_PATH))
{
Directory.CreateDirectory(ATLAS_ROOT_PATH);
}
EditorUtility.DisplayProgressBar("构建图集", "生成图集文件...", 0f);
int count = config.Atlases.Count;
// 第一阶段:为每个图集写入 .spriteatlasv2 YAML 文件并逐个同步导入
for (int i = 0; i < count; i++)
{
var atlas = config.Atlases[i];
EditorUtility.DisplayProgressBar("构建图集", $"生成 {atlas.Name}...", (float)i / count * 0.5f);
string atlasPath = $"{ATLAS_ROOT_PATH}/{atlas.Name}.spriteatlasv2";
WriteV2AtlasFile(atlas);
AssetDatabase.ImportAsset(atlasPath, ImportAssetOptions.ForceSynchronousImport);
}
// 第二阶段:统一打包
EditorUtility.DisplayProgressBar("构建图集", "打包图集...", 0.6f);
List<SpriteAtlas> allAtlases = new List<SpriteAtlas>();
for (int i = 0; i < count; i++)
{
var atlas = config.Atlases[i];
string atlasPath = $"{ATLAS_ROOT_PATH}/{atlas.Name}.spriteatlasv2";
SpriteAtlas spriteAtlas = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(atlasPath);
if (spriteAtlas != null)
{
allAtlases.Add(spriteAtlas);
}
else
{
Debug.LogWarning($"[AtlasBuilderEditor] 图集加载失败,跳过打包: {atlasPath}");
}
}
if (allAtlases.Count > 0)
{
SpriteAtlasUtility.PackAtlases(allAtlases.ToArray(), EditorUserBuildSettings.activeBuildTarget);
}
// 第三阶段:重新生成 atlas_mapping.bytes运行时通过此文件查找 sprite→atlas 映射)
EditorUtility.DisplayProgressBar("构建图集", "生成 Atlas Mapping...", 0.9f);
AtlasMapper.Mapping();
AssetDatabase.Refresh();
EditorUtility.ClearProgressBar();
EditorUtility.DisplayDialog("提示", $"已构建 {allAtlases.Count} 个图集并保存配置\n已重新生成 Atlas Mapping", "确定");
}
/// <summary>
/// 直接写入 .spriteatlasv2 YAML 文件V2 格式)
/// </summary>
private void WriteV2AtlasFile(AtlasData atlasData)
{
string atlasPath = $"{ATLAS_ROOT_PATH}/{atlasData.Name}.spriteatlasv2";
// 收集所有有效 Sprite 的 GUID 和 localFileID
var packableEntries = new List<string>();
foreach (string spritePath in atlasData.SpritePaths)
{
Sprite sprite = AssetDatabase.LoadAssetAtPath<Sprite>(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");
File.WriteAllText(atlasPath, sb.ToString());
}
/// <summary>
/// 清理旧版 .spriteatlas 文件V1格式只保留 .spriteatlasv2
/// </summary>
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";
WriteV2AtlasFile(atlasData);
AssetDatabase.ImportAsset(atlasPath, ImportAssetOptions.ForceSynchronousImport);
SpriteAtlas spriteAtlas = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(atlasPath);
if (spriteAtlas != null)
{
SpriteAtlasUtility.PackAtlases(new[] { spriteAtlas }, EditorUserBuildSettings.activeBuildTarget);
}
}
private void LoadConfig()
{
if (File.Exists(CONFIG_PATH))
{
string json = File.ReadAllText(CONFIG_PATH);
config = JsonUtility.FromJson<AtlasConfig>(json);
}
else
{
config = new AtlasConfig { Atlases = new List<AtlasData>() };
SaveConfig();
}
}
private void SaveConfig()
{
// 保存JSON配置
string dir = Path.GetDirectoryName(CONFIG_PATH);
if (!Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
}
string json = JsonUtility.ToJson(config, true);
File.WriteAllText(CONFIG_PATH, json);
// 生成Thrift Bytes配置
SaveConfigToThriftBytes();
AssetDatabase.Refresh();
}
/// <summary>
/// 保存配置到Thrift Bytes格式
/// </summary>
private void 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<Byway.Thrift.Data.ArtAtlasInfo>(),
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<string>(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();
}
// 写入文件
File.WriteAllBytes(BYTES_PATH, bytesData);
startTime.Stop();
Debug.Log($"[AtlasBuilderEditor] ✅ Atlas Bytes生成成功\n" +
$" 路径: {BYTES_PATH}\n" +
$" 图集数量: {thriftConfig.Atlases.Count}\n" +
$" 文件大小: {bytesData.Length / 1024f:F2} KB\n" +
$" 耗时: {startTime.ElapsedMilliseconds} ms");
}
catch (System.Exception ex)
{
Debug.LogError($"[AtlasBuilderEditor] ❌ 生成Atlas Bytes失败: {ex.Message}\n{ex.StackTrace}");
}
}
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<Sprite>(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<string> allAssignedPaths = new HashSet<string>();
foreach (var atlas in config.Atlases)
{
foreach (string path in atlas.SpritePaths)
{
allAssignedPaths.Add(path);
}
}
MarkAssignedRecursive(spriteRoot, allAssignedPaths);
}
private bool MarkAssignedRecursive(SpriteFolder folder, HashSet<string> 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<SpriteAsset> GetSelectedSprites()
{
if (!hideAssignedSprites)
{
return selectedSpriteAssets;
}
HashSet<SpriteAsset> result = new HashSet<SpriteAsset>();
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<SpriteFolder> SubFolders = new List<SpriteFolder>();
public List<SpriteAsset> Sprites = new List<SpriteAsset>();
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;
}
}