Files
5.24BuYu/Assets/Editor/SpriteAtlasBuilderWindow.cs
T
2026-10-09 20:20:14 +08:00

296 lines
12 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 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;
/// <summary>
/// 预制体图集打包工具。
///
/// 【用途】
/// 选中若干 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 重建内容
/// </summary>
public class SpriteAtlasBuilderWindow : EditorWindow
{
[MenuItem("Tools/图集工具")]
public static void Open()
{
var w = GetWindow<SpriteAtlasBuilderWindow>("图集工具");
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<SpriteInfo> _sprites = new List<SpriteInfo>();
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<string> { "不加入(稍后手动)" };
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();
}
}
/// <summary>扫描选中的预制体,收集全部 Sprite 引用(去重、排除引擎内置资源)</summary>
private void Scan()
{
_sprites.Clear();
var seen = new HashSet<Object>();
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<GameObject>(path);
if (go == null) continue;
prefabCount++;
foreach (var comp in go.GetComponentsInChildren<Component>(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} 张图"));
}
}
/// <summary>按总像素估算图集边长(利用率 70%,向上取 2 的幂,范围 256~4096)</summary>
private static int CalcAutoSize(List<SpriteInfo> 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);
}
/// <summary>创建图集并(可选)加入 Addressables 组</summary>
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)后生效。", "确定");
}
}