using System.Collections.Generic; using System.IO; using System.Linq; using UnityEditor; using UnityEditor.AddressableAssets; using UnityEditor.AddressableAssets.Settings; using UnityEditor.U2D; using UnityEngine; using UnityEngine.U2D; /// /// 预制体图集打包工具。 /// /// 【用途】 /// 选中若干 UI 预制体(如大厅散包的各子面板)→ 扫描它们引用的全部 Sprite → /// 一键创建 SpriteAtlas 并把这些图合进去(自动按总像素估算合适的图集尺寸), /// 避免散包后每张小图单独打包/加载,也避免多个包重复携带同一批图。 /// /// 【使用】 /// 1. Project 里选中一个或多个预制体(子面板 prefab) /// 2. 菜单 Tools → 图集工具 → [扫描选中预制体] /// 3. 检查图片列表(会自动排除 Unity 内置资源;可勾选排除大背景图) /// 4. 设置输出目录/图集名/尺寸(默认自动计算)/ Padding /// 5. 选择要加入的 Addressables 组(强烈建议:与使用这些图的 prefab 同组, /// 否则图集不进组会被引用它的每个 bundle 各复制一份!) /// 6. [创建图集] → 重建 Addressables 内容后生效 /// /// 【注意】 /// - 打进图集的图片无需改 TextureImporter(图集优先于单图导入设置) /// - 单图超过图集最大尺寸时 SpriteAtlas 会自动分页(multi-atlas) /// - 创建后记得 Build → New Build 重建内容 /// public class SpriteAtlasBuilderWindow : EditorWindow { [MenuItem("Tools/图集工具")] public static void Open() { var w = GetWindow("图集工具"); w.minSize = new Vector2(520f, 560f); } private class SpriteInfo { public Sprite sprite; public string path; public int w, h; public bool big; // 边长 ≥ 512 的大图(默认排除) } private readonly List _sprites = new List(); private Vector2 _scroll; private string _outputDir = "Assets/MyGame/aa/UI/Atlases"; private string _atlasName = "UIAtlas"; private int _maxSizeIndex = 0; private readonly string[] _maxSizeOptions = { "自动(按总像素)", "1024", "2048", "4096" }; private int _padding = 4; private bool _excludeBig = true; private string[] _groupNames = { "不加入(稍后手动)" }; private int _groupIndex = 0; private void OnEnable() => RefreshGroups(); private void RefreshGroups() { var list = new List { "不加入(稍后手动)" }; try { var s = AddressableAssetSettingsDefaultObject.Settings; if (s != null) list.AddRange(s.groups.Where(g => !g.ReadOnly).Select(g => g.Name)); } catch { } _groupNames = list.ToArray(); } private void OnGUI() { EditorGUILayout.HelpBox( "1. Project 里选中一个或多个 UI 预制体\n" + "2. 点 [扫描选中预制体] 收集引用的 Sprite\n" + "3. 创建图集时选择与这些预制体相同的 Addressables 组\n" + "(图集不进组会被引用它的每个 bundle 复制一份)", MessageType.Info); if (GUILayout.Button("扫描选中预制体", GUILayout.Height(32))) Scan(); EditorGUILayout.Space(4); // ---- 扫描结果 ---- if (_sprites.Count > 0) { var included = _sprites.Where(s => !(_excludeBig && s.big)).ToList(); long totalPixels = included.Sum(s => (long)s.w * s.h); long bigCount = _sprites.Count(s => s.big); int autoSize = CalcAutoSize(included, _padding); EditorGUILayout.LabelField($"共 {_sprites.Count} 张图" + (bigCount > 0 ? $"(其中 {bigCount} 张大图,见下方排除开关)" : ""), EditorStyles.boldLabel); EditorGUILayout.LabelField($"合计像素: {totalPixels / 10000f:F1} 万 | 自动图集尺寸: {autoSize}"); _excludeBig = EditorGUILayout.ToggleLeft("排除大图(边长≥512,背景图等通常不进图集)", _excludeBig); _scroll = EditorGUILayout.BeginScrollView(_scroll, GUILayout.MinHeight(180)); foreach (var s in _sprites) { var style = s.big ? new GUIStyle(EditorStyles.label) { normal = { textColor = Color.yellow } } : EditorStyles.label; GUILayout.Label($"{s.w}×{s.h}{(s.big ? " [大图]" : "")} {s.path}", style); } EditorGUILayout.EndScrollView(); } else { EditorGUILayout.HelpBox("尚未扫描(或选中对象里没有预制体)", MessageType.None); } EditorGUILayout.Space(8); // ---- 设置 ---- EditorGUILayout.LabelField("图集设置", EditorStyles.boldLabel); _outputDir = EditorGUILayout.TextField("输出目录", _outputDir); _atlasName = EditorGUILayout.TextField("图集名称", _atlasName); _maxSizeIndex = EditorGUILayout.Popup("最大尺寸", _maxSizeIndex, _maxSizeOptions); _padding = EditorGUILayout.IntSlider("图集内边距(Padding)", _padding, 1, 16); EditorGUILayout.Space(4); EditorGUILayout.LabelField("加入 Addressables 组(建议与预制体同组)", EditorStyles.boldLabel); _groupIndex = EditorGUILayout.Popup("目标组", _groupIndex, _groupNames); EditorGUILayout.Space(8); using (new EditorGUI.DisabledScope(_sprites.Count == 0)) { if (GUILayout.Button("创建图集", GUILayout.Height(36))) Create(); } } /// 扫描选中的预制体,收集全部 Sprite 引用(去重、排除引擎内置资源) private void Scan() { _sprites.Clear(); var seen = new HashSet(); int prefabCount = 0; foreach (var obj in Selection.objects) { string path = AssetDatabase.GetAssetPath(obj); if (string.IsNullOrEmpty(path) || !path.EndsWith(".prefab")) continue; var go = AssetDatabase.LoadAssetAtPath(path); if (go == null) continue; prefabCount++; foreach (var comp in go.GetComponentsInChildren(true)) { if (comp == null) continue; var so = new SerializedObject(comp); var prop = so.GetIterator(); while (prop.NextVisible(true)) { if (prop.propertyType != SerializedPropertyType.ObjectReference) continue; var sp = prop.objectReferenceValue as Sprite; if (sp == null || !seen.Add(sp)) continue; string p = AssetDatabase.GetAssetPath(sp); if (string.IsNullOrEmpty(p) || p.StartsWith("Library/")) continue; // 引擎内置 _sprites.Add(new SpriteInfo { sprite = sp, path = p, w = Mathf.CeilToInt(sp.rect.width), h = Mathf.CeilToInt(sp.rect.height), big = sp.rect.width >= 512 || sp.rect.height >= 512 }); } } } if (prefabCount == 0) ShowNotification(new GUIContent("选中对象里没有预制体")); else if (_sprites.Count == 0) ShowNotification(new GUIContent($"扫描了 {prefabCount} 个预制体,未发现 Sprite 引用")); else { // 默认图集名取第一个预制体名 if (_atlasName == "UIAtlas" || string.IsNullOrEmpty(_atlasName)) { var first = AssetDatabase.GetAssetPath(Selection.objects[0]); _atlasName = Path.GetFileNameWithoutExtension(first) + "Atlas"; } ShowNotification(new GUIContent($"扫描 {prefabCount} 个预制体,发现 {_sprites.Count} 张图")); } } /// 按总像素估算图集边长(利用率 70%,向上取 2 的幂,范围 256~4096) private static int CalcAutoSize(List sprites, int padding) { long area = sprites.Sum(s => (long)(s.w + padding) * (s.h + padding)); double need = area / 0.7; int side = Mathf.CeilToInt(Mathf.Sqrt((float)need)); int pow = 256; while (pow < side && pow < 4096) pow <<= 1; return Mathf.Clamp(pow, 256, 4096); } /// 创建图集并(可选)加入 Addressables 组 private void Create() { var included = _sprites.Where(s => !(_excludeBig && s.big)).ToList(); if (included.Count == 0) { ShowNotification(new GUIContent("没有可打包的图片(全被排除了?)")); return; } if (!AssetDatabase.IsValidFolder(_outputDir)) { // 逐级创建目录 string cur = "Assets"; foreach (var seg in _outputDir.Substring("Assets/".Length).Split('/')) { string next = cur + "/" + seg; if (!AssetDatabase.IsValidFolder(next)) AssetDatabase.CreateFolder(cur, seg); cur = next; } } string atlasPath = $"{_outputDir}/{_atlasName}.spriteatlas"; if (File.Exists(atlasPath) && !EditorUtility.DisplayDialog("覆盖确认", $"已存在同名图集:\n{atlasPath}\n继续将覆盖重建。", "覆盖", "取消")) return; int maxSize = _maxSizeIndex == 0 ? CalcAutoSize(included, _padding) : int.Parse(_maxSizeOptions[_maxSizeIndex]); // SpriteAtlas 不是 ScriptableObject(继承自 Object),直接 new 后 CreateAsset var atlas = new SpriteAtlas(); atlas.SetIncludeInBuild(true); // padding 通过序列化字段写入(m_EditorData.padding,该版本无公开编辑 API) var so = new SerializedObject(atlas); var padProp = so.FindProperty("m_EditorData.padding"); if (padProp != null) { padProp.intValue = _padding; so.ApplyModifiedPropertiesWithoutUndo(); } // Android / iOS:ASTC 6x6(移动端体积/质量均衡) foreach (var platform in new[] { "Android", "iPhone" }) { var ps = atlas.GetPlatformSettings(platform); ps.overridden = true; ps.maxTextureSize = maxSize; ps.format = TextureImporterFormat.ASTC_6x6; ps.compressionQuality = 50; atlas.SetPlatformSettings(ps); } atlas.Add(included.Select(s => (Object)s.sprite).ToArray()); AssetDatabase.CreateAsset(atlas, atlasPath); EditorUtility.SetDirty(atlas); AssetDatabase.SaveAssets(); // 可选:加入 Addressables 组 string groupMsg = ""; if (_groupIndex > 0) { try { var settings = AddressableAssetSettingsDefaultObject.Settings; var group = settings.FindGroup(_groupNames[_groupIndex]); if (group != null) { settings.CreateOrMoveEntry(AssetDatabase.AssetPathToGUID(atlasPath), group); AssetDatabase.SaveAssets(); groupMsg = $"\n已加入组: {group.Name}"; } } catch (System.Exception e) { groupMsg = $"\n加入组失败(可手动添加): {e.Message}"; } } Debug.Log($"[图集工具] 创建完成: {atlasPath}\n图片 {included.Count} 张 | 最大尺寸 {maxSize} | Padding {_padding}{groupMsg}"); EditorUtility.DisplayDialog("图集工具", $"创建完成!\n\n{atlasPath}\n图片 {included.Count} 张\n最大尺寸 {maxSize}\nPadding {_padding}{groupMsg}\n\n" + "提醒:重建 Addressables 内容(Build → New Build)后生效。", "确定"); } }