296 lines
12 KiB
C#
296 lines
12 KiB
C#
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)后生效。", "确定");
|
|||
|
|
}
|
|||
|
|
}
|