新增Altas图集编辑器

增加可视化定制化的路径配置
增加图集冗余机制判断 自动更新自动移除冗余依赖图集
增加各平台打包图集设定
增加V2和v1图集切换打包 ps:新旧想用哪个都行
This commit is contained in:
陈思海
2025-03-14 15:52:23 +08:00
parent e9d6c27006
commit c728dc4c0f
18 changed files with 842 additions and 623 deletions

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: 6b4dd55e1b47da54697b93c6bc2bd337 guid: 9d6134158c52933418338d95eebd1c39
folderAsset: yes folderAsset: yes
DefaultImporter: DefaultImporter:
externalObjects: {} externalObjects: {}

View File

@@ -0,0 +1,20 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!612988286 &1
SpriteAtlasAsset:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name:
serializedVersion: 2
m_MasterAtlas: {fileID: 0}
m_ImporterData:
packables:
- {fileID: 21300000, guid: c95935206d18f42399e9881ce6bdf61c, type: 3}
- {fileID: 21300000, guid: 1f44cb90b6ae648c783dac573bfe9cd0, type: 3}
- {fileID: 21300000, guid: bfad0e64f8ef1491c8def7e679187b89, type: 3}
- {fileID: 21300000, guid: a43cb397a444ce44f9becc183ced96ed, type: 3}
- {fileID: 21300000, guid: f43899c31dd434901b0422f98bcde88b, type: 3}
- {fileID: 21300000, guid: 01ccaea9b49214edda39f03f648b5b8b, type: 3}
m_IsVariant: 0

View File

@@ -0,0 +1,69 @@
fileFormatVersion: 2
guid: e6770285dceffae48b253f30f09cd3bc
SpriteAtlasImporter:
externalObjects: {}
textureSettings:
serializedVersion: 2
anisoLevel: 1
compressionQuality: 50
maxTextureSize: 2048
textureCompression: 0
filterMode: 1
generateMipMaps: 0
readable: 0
crunchedCompression: 0
sRGB: 1
platformSettings:
- serializedVersion: 3
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: 50
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 1
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: iPhone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: 49
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 1
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: WebGL
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: 50
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 1
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
packingSettings:
serializedVersion: 2
padding: 4
blockOffset: 1
allowAlphaSplitting: 0
enableRotation: 1
enableTightPacking: 1
enableAlphaDilation: 1
secondaryTextureSettings: {}
variantMultiplier: 1
bindAsDefault: 1
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,58 +0,0 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!612988286 &4096168566840163508
SpriteAtlasAsset:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: UIRaw_Atlas_Battle
m_MasterAtlas: {fileID: 0}
m_ImporterData:
serializedVersion: 2
textureSettings:
serializedVersion: 2
anisoLevel: 0
compressionQuality: 0
maxTextureSize: 0
textureCompression: 0
filterMode: 1
generateMipMaps: 0
readable: 0
crunchedCompression: 0
sRGB: 1
platformSettings:
- serializedVersion: 3
m_BuildTarget: iPhone
m_MaxTextureSize: 2048
m_ResizeAlgorithm: 0
m_TextureFormat: 50
m_TextureCompression: 1
m_CompressionQuality: 100
m_CrunchedCompression: 0
m_AllowsAlphaSplitting: 0
m_Overridden: 1
m_AndroidETC2FallbackOverride: 0
m_ForceMaximumCompressionQuality_BC6H_BC7: 0
packingSettings:
serializedVersion: 2
padding: 2
blockOffset: 1
allowAlphaSplitting: 0
enableRotation: 1
enableTightPacking: 0
enableAlphaDilation: 0
secondaryTextureSettings: {}
variantMultiplier: 1
packables:
- {fileID: 21300000, guid: f43899c31dd434901b0422f98bcde88b, type: 3}
- {fileID: 21300000, guid: 01ccaea9b49214edda39f03f648b5b8b, type: 3}
- {fileID: 21300000, guid: 1f44cb90b6ae648c783dac573bfe9cd0, type: 3}
- {fileID: 21300000, guid: bfad0e64f8ef1491c8def7e679187b89, type: 3}
- {fileID: 21300000, guid: af67f6c46933441daaa64d6880890586, type: 3}
- {fileID: 21300000, guid: c95935206d18f42399e9881ce6bdf61c, type: 3}
- {fileID: 21300000, guid: a43cb397a444ce44f9becc183ced96ed, type: 3}
bindAsDefault: 1
isAtlasV2: 1
cachedData: {fileID: 0}
m_IsVariant: 0

View File

@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: f8e251863479c4648a2271dcc7504237
SpriteAtlasImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 16fac2fa602633346b6f45f4362f28f7
timeCreated: 1738748198

View File

@@ -0,0 +1,46 @@
namespace TEngine.Editor
{
#if UNITY_EDITOR
using UnityEngine;
using UnityEditor;
[HybridCLR.Editor.Settings.FilePath("ProjectSettings/AtlasConfiguration.asset")]
public class AtlasConfiguration : HybridCLR.Editor.Settings.ScriptableSingleton<AtlasConfiguration>
{
[Header("目录设置")]
[Tooltip("生成的图集输出目录")]
public string outputAtlasDir = "Assets/AssetArt/Atlas";
[Tooltip("需要生成图集的UI根目录")]
public string sourceAtlasRoot = "Assets/AssetRaw/UIRaw/Atlas";
[Tooltip("不需要生成图集的UI目录")]
public string excludeFolder = "Assets/AssetRaw/UIRaw/Raw";
[Header("平台格式设置")]
public TextureImporterFormat androidFormat = TextureImporterFormat.ASTC_6x6;
public TextureImporterFormat iosFormat = TextureImporterFormat.ASTC_5x5;
public TextureImporterFormat webglFormat = TextureImporterFormat.ASTC_6x6;
[Header("PackingSetting")]
public int padding = 2;
public bool enableRotation = true;
public int blockOffset = 1;
public bool tightPacking = true;
[Header("其他设置")]
[Range(0, 100)]
public int compressionQuality = 50;
public bool autoGenerate = true;
public bool enableLogging = true;
public bool enableV2 = true;
[Header("排除关键词")]
public string[] excludeKeywords = { "_Delete", "_Temp" };
}
#endif
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f529ff1e114e8ba4cb189f4233102cd5
timeCreated: 1738748213

View File

@@ -0,0 +1,162 @@
namespace TEngine.Editor
{
#if UNITY_EDITOR
using System;
using UnityEditor;
using UnityEngine;
public class AtlasConfigWindow : EditorWindow
{
[MenuItem("Tools/图集工具/配置面板")]
public static void ShowWindow()
{
GetWindow<AtlasConfigWindow>("Atlas Config").minSize = new Vector2(450, 400);
}
private Vector2 _scrollPosition;
private int[] _paddingEnum = new int[] { 2, 4, 8 };
private bool _showExcludeKeywords = false; // 新增折叠状态变量
private void OnGUI()
{
var config = AtlasConfiguration.Instance;
using (var scrollScope = new EditorGUILayout.ScrollViewScope(_scrollPosition))
{
_scrollPosition = scrollScope.scrollPosition;
EditorGUI.BeginChangeCheck();
DrawFolderSettings(config);
DrawPlatformSettings(config);
DrawPackingSettings(config);
DrawAdvancedSettings(config);
if (EditorGUI.EndChangeCheck())
{
AtlasConfiguration.Save(true);
AssetDatabase.Refresh();
}
DrawActionButtons();
}
}
private void DrawFolderSettings(AtlasConfiguration config)
{
GUILayout.Label("目录设置", EditorStyles.boldLabel);
config.outputAtlasDir = DrawFolderField("输出目录", config.outputAtlasDir);
config.sourceAtlasRoot = DrawFolderField("收集目录", config.sourceAtlasRoot);
config.excludeFolder = DrawFolderField("排除目录", config.excludeFolder);
EditorGUILayout.Space();
}
private string DrawFolderField(string label, string path)
{
using (new EditorGUILayout.HorizontalScope())
{
path = EditorGUILayout.TextField(label, path);
if (GUILayout.Button("选择", GUILayout.Width(60)))
{
var newPath = EditorUtility.OpenFolderPanel(label, Application.dataPath, "");
if (!string.IsNullOrEmpty(newPath))
{
path = "Assets" + newPath.Substring(Application.dataPath.Length);
}
}
}
return path;
}
private void DrawPlatformSettings(AtlasConfiguration config)
{
GUILayout.Label("平台设置", EditorStyles.boldLabel);
config.androidFormat = (TextureImporterFormat)EditorGUILayout.EnumPopup("Android 格式", config.androidFormat);
config.iosFormat = (TextureImporterFormat)EditorGUILayout.EnumPopup("iOS 格式", config.iosFormat);
config.webglFormat = (TextureImporterFormat)EditorGUILayout.EnumPopup("WebGL 格式", config.webglFormat);
config.compressionQuality = EditorGUILayout.IntSlider("压缩质量", config.compressionQuality, 0, 100);
EditorGUILayout.Space();
}
private void DrawPackingSettings(AtlasConfiguration config)
{
GUILayout.Label("PackingSetting", EditorStyles.boldLabel);
config.padding = EditorGUILayout.IntPopup("Padding",
config.padding,
Array.ConvertAll(_paddingEnum, x => x.ToString()),
_paddingEnum);
config.blockOffset = EditorGUILayout.IntField("BlockOffset", config.blockOffset);
config.enableRotation = EditorGUILayout.Toggle("Enable Rotation", config.enableRotation);
config.tightPacking = EditorGUILayout.Toggle("剔除透明区域", config.tightPacking);
EditorGUILayout.Space();
}
private void DrawAdvancedSettings(AtlasConfiguration config)
{
GUILayout.Label("高级设置", EditorStyles.boldLabel);
config.autoGenerate = EditorGUILayout.Toggle("自动生成", config.autoGenerate);
config.enableLogging = EditorGUILayout.Toggle("启用日志", config.enableLogging);
config.enableV2 = EditorGUILayout.Toggle("启用V2打包", config.enableV2);
// 优化后的排除关键词显示
_showExcludeKeywords = EditorGUILayout.BeginFoldoutHeaderGroup(_showExcludeKeywords, "排除关键词");
if (_showExcludeKeywords)
{
int keywordCount = config.excludeKeywords?.Length ?? 0;
int newCount = EditorGUILayout.IntField("数量", keywordCount);
// 调整数组大小
if (newCount != keywordCount)
{
Array.Resize(ref config.excludeKeywords, newCount);
}
// 绘制每个元素
for (int i = 0; i < newCount; i++)
{
config.excludeKeywords[i] = EditorGUILayout.TextField($"关键词 {i + 1}",
config.excludeKeywords[i] ?? "");
}
// 添加快捷按钮
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
if (GUILayout.Button("添加", GUILayout.Width(60)))
{
Array.Resize(ref config.excludeKeywords, newCount + 1);
}
if (GUILayout.Button("清空", GUILayout.Width(60)))
{
config.excludeKeywords = Array.Empty<string>();
}
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndFoldoutHeaderGroup();
EditorGUILayout.Space();
}
private void DrawActionButtons()
{
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("立即生成所有图集", GUILayout.Height(30)))
{
EditorSpriteSaveInfo.ForceGenerateAll();
}
if (GUILayout.Button("清空缓存", GUILayout.Height(30)))
{
EditorSpriteSaveInfo.ClearCache();
}
}
}
}
#endif
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5a0f8acf9a5e4034f9f28fc384c5ce89
timeCreated: 1738748236

View File

@@ -0,0 +1,361 @@
namespace TEngine.Editor
{
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEditor.U2D;
using UnityEngine;
using UnityEngine.U2D;
public static class EditorSpriteSaveInfo
{
private static readonly HashSet<string> _dirtyAtlasNames = new HashSet<string>();
private static readonly Dictionary<string, List<string>> _atlasMap = new Dictionary<string, List<string>>();
private static bool _initialized;
private static AtlasConfiguration Config => AtlasConfiguration.Instance;
static EditorSpriteSaveInfo()
{
EditorApplication.update += OnUpdate;
Initialize();
}
private static void Initialize()
{
if (_initialized) return;
ScanExistingSprites();
_initialized = true;
}
public static void OnImportSprite(string assetPath)
{
if (!ShouldProcess(assetPath)) return;
var atlasName = GetAtlasName(assetPath);
if (string.IsNullOrEmpty(atlasName)) return;
if (!_atlasMap.TryGetValue(atlasName, out var list))
{
list = new List<string>();
_atlasMap[atlasName] = list;
}
if (!list.Contains(assetPath))
{
list.Add(assetPath);
MarkDirty(atlasName);
MarkParentAtlasesDirty(assetPath);
}
}
public static void OnDeleteSprite(string assetPath)
{
if (!ShouldProcess(assetPath)) return;
var atlasName = GetAtlasName(assetPath);
if (string.IsNullOrEmpty(atlasName)) return;
if (_atlasMap.TryGetValue(atlasName, out var list))
{
if (list.Remove(assetPath))
{
MarkDirty(atlasName);
MarkParentAtlasesDirty(assetPath);
}
}
}
[MenuItem("Tools/图集工具/ForceGenerateAll")]
public static void ForceGenerateAll()
{
_atlasMap.Clear();
ScanExistingSprites();
_dirtyAtlasNames.UnionWith(_atlasMap.Keys);
ProcessDirtyAtlases(true);
}
public static void ClearCache()
{
_dirtyAtlasNames.Clear();
_atlasMap.Clear();
AssetDatabase.Refresh();
}
public static void MarkParentAtlasesDirty(string assetPath)
{
var currentPath = Path.GetDirectoryName(assetPath);
var rootPath = Config.sourceAtlasRoot.Replace("\\", "/").TrimEnd('/');
while (currentPath != null && currentPath.StartsWith(rootPath))
{
var parentAtlasName = GetAtlasNameForDirectory(currentPath);
if (!string.IsNullOrEmpty(parentAtlasName))
{
MarkDirty(parentAtlasName);
}
currentPath = Path.GetDirectoryName(currentPath);
}
}
private static void OnUpdate()
{
if (_dirtyAtlasNames.Count > 0)
{
ProcessDirtyAtlases();
}
}
private static void ProcessDirtyAtlases(bool force = false)
{
try
{
AssetDatabase.StartAssetEditing();
foreach (var atlasName in _dirtyAtlasNames.ToList())
{
if (force || ShouldUpdateAtlas(atlasName))
{
GenerateAtlas(atlasName);
}
_dirtyAtlasNames.Remove(atlasName);
}
}
finally
{
AssetDatabase.StopAssetEditing();
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
}
private static void GenerateAtlas(string atlasName)
{
var outputPath = $"{Config.outputAtlasDir}/{atlasName}.spriteatlas";
SpriteAtlasAsset spriteAtlasAsset = default;
SpriteAtlas atlas = new SpriteAtlas();
if (Config.enableV2)
{
outputPath = $"{Config.outputAtlasDir}/{atlasName}.spriteatlasv2";
if (!File.Exists(outputPath))
{
spriteAtlasAsset = new SpriteAtlasAsset();
}
else
{
spriteAtlasAsset = SpriteAtlasAsset.Load(outputPath);
atlas = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(outputPath);
if (atlas != null)
{
var olds = atlas.GetPackables();
if (olds != null) spriteAtlasAsset.Remove(olds);
}
}
}
var sprites = LoadValidSprites(atlasName);
EnsureOutputDirectory();
if (sprites.Count == 0)
{
DeleteAtlas(outputPath);
return;
}
if (Config.enableV2)
{
spriteAtlasAsset.Add(sprites.ToArray());
SpriteAtlasAsset.Save(spriteAtlasAsset, outputPath);
AssetDatabase.Refresh();
SpriteAtlasImporter sai = (SpriteAtlasImporter)AssetImporter.GetAtPath(outputPath);
ConfigureAtlasV2Settings(sai);
AssetDatabase.WriteImportSettingsIfDirty(outputPath);
}
else
{
ConfigureAtlasSettings(atlas);
atlas.Add(sprites.ToArray());
atlas.SetIsVariant(false);
AssetDatabase.CreateAsset(atlas, outputPath);
}
if (Config.enableLogging)
Debug.Log($"Generated atlas: {atlasName} ({sprites.Count} sprites)");
}
private static List<Sprite> LoadValidSprites(string atlasName)
{
return _atlasMap[atlasName]
.Where(File.Exists)
.Select(p => AssetDatabase.LoadAssetAtPath<Sprite>(p))
.Where(s => s != null)
.ToList();
}
private static void ConfigureAtlasV2Settings(SpriteAtlasImporter atlasImporter)
{
void SetPlatform(string platform, TextureImporterFormat format)
{
var settings = atlasImporter.GetPlatformSettings(platform);
if (settings == null) return;
;
settings.overridden = true;
settings.format = format;
settings.compressionQuality = Config.compressionQuality;
atlasImporter.SetPlatformSettings(settings);
}
SetPlatform("Android", Config.androidFormat);
SetPlatform("iPhone", Config.iosFormat);
SetPlatform("WebGL", Config.webglFormat);
var packingSettings = new SpriteAtlasPackingSettings
{
padding = Config.padding,
enableRotation = Config.enableRotation,
blockOffset = Config.blockOffset,
enableTightPacking = Config.tightPacking,
enableAlphaDilation = true
};
atlasImporter.packingSettings = packingSettings;
}
private static void ConfigureAtlasSettings(SpriteAtlas atlas)
{
void SetPlatform(string platform, TextureImporterFormat format)
{
var settings = atlas.GetPlatformSettings(platform);
settings.overridden = true;
settings.format = format;
settings.compressionQuality = Config.compressionQuality;
atlas.SetPlatformSettings(settings);
}
SetPlatform("Android", Config.androidFormat);
SetPlatform("iPhone", Config.iosFormat);
SetPlatform("WebGL", Config.webglFormat);
var packingSettings = new SpriteAtlasPackingSettings
{
padding = Config.padding,
enableRotation = Config.enableRotation,
blockOffset = Config.blockOffset,
};
atlas.SetPackingSettings(packingSettings);
}
private static string GetAtlasName(string assetPath)
{
var normalizedPath = assetPath.Replace("\\", "/");
var rootPath = Config.sourceAtlasRoot.Replace("\\", "/").TrimEnd('/');
if (!normalizedPath.StartsWith(rootPath + "/")) return null;
var relativePath = normalizedPath
.Substring(rootPath.Length + 1)
.Split('/');
if (relativePath.Length < 2) return null;
var directories = relativePath.Take(relativePath.Length - 1);
var atlasNamePart = string.Join("_", directories);
var rootFolderName = Path.GetFileName(rootPath);
return $"{rootFolderName}_{atlasNamePart}";
}
private static bool ShouldProcess(string assetPath)
{
return IsImageFile(assetPath) && !IsExcluded(assetPath);
}
private static bool IsExcluded(string path)
{
return path.StartsWith(Config.excludeFolder) ||
Config.excludeKeywords.Any(k => path.IndexOf(k, StringComparison.OrdinalIgnoreCase) >= 0);
}
private static bool IsImageFile(string path)
{
var ext = Path.GetExtension(path).ToLower();
return ext == ".png" || ext == ".jpg" || ext == ".jpeg";
}
private static void MarkDirty(string atlasName)
{
_dirtyAtlasNames.Add(atlasName);
}
private static bool ShouldUpdateAtlas(string atlasName)
{
// var outputPath = $"{Config.outputAtlasDir}/{atlasName}.spriteatlas";
return true;
}
private static DateTime GetLatestSpriteTime(string atlasName)
{
return _atlasMap[atlasName]
.Select(p => new FileInfo(p).LastWriteTime)
.DefaultIfEmpty()
.Max();
}
private static void DeleteAtlas(string path)
{
if (File.Exists(path))
{
AssetDatabase.DeleteAsset(path);
if (Config.enableLogging)
Debug.Log($"Deleted empty atlas: {Path.GetFileName(path)}");
}
}
private static void EnsureOutputDirectory()
{
if (!Directory.Exists(Config.outputAtlasDir))
{
Directory.CreateDirectory(Config.outputAtlasDir);
AssetDatabase.Refresh();
}
}
private static void ScanExistingSprites()
{
var guids = AssetDatabase.FindAssets("t:Sprite", new[] { Config.sourceAtlasRoot });
foreach (var guid in guids)
{
var path = AssetDatabase.GUIDToAssetPath(guid);
if (ShouldProcess(path))
{
OnImportSprite(path);
}
}
}
private static string GetAtlasNameForDirectory(string directoryPath)
{
var normalizedPath = directoryPath.Replace("\\", "/");
var rootPath = Config.sourceAtlasRoot.Replace("\\", "/").TrimEnd('/');
if (!normalizedPath.StartsWith(rootPath + "/")) return null;
var relativePath = normalizedPath
.Substring(rootPath.Length + 1)
.Split('/');
var atlasNamePart = string.Join("_", relativePath);
var rootFolderName = Path.GetFileName(rootPath);
return $"{rootFolderName}_{atlasNamePart}";
}
}
#endif
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6e0aa32db2943f742a839708e928fda4
timeCreated: 1738748262

View File

@@ -0,0 +1,137 @@
using System;
using System.IO;
namespace TEngine.Editor
{
using UnityEditor;
using UnityEngine;
public class SpritePostprocessor : AssetPostprocessor
{
private static void OnPostprocessAllAssets(
string[] importedAssets,
string[] deletedAssets,
string[] movedAssets,
string[] movedFromAssetPaths)
{
var config = AtlasConfiguration.Instance;
if (!config.autoGenerate) return;
try
{
ProcessAssetChanges(
importedAssets: importedAssets,
deletedAssets: deletedAssets,
movedAssets: movedAssets,
movedFromPaths: movedFromAssetPaths
);
}
catch (Exception e)
{
Debug.LogError($"Atlas processing error: {e.Message}\n{e.StackTrace}");
}
finally
{
AssetDatabase.Refresh();
}
}
private static void ProcessAssetChanges(
string[] importedAssets,
string[] deletedAssets,
string[] movedAssets,
string[] movedFromPaths)
{
ProcessAssets(importedAssets, (path) =>
{
EditorSpriteSaveInfo.OnImportSprite(path);
LogProcessed("[Added]", path);
});
ProcessAssets(deletedAssets, (path) =>
{
EditorSpriteSaveInfo.OnDeleteSprite(path);
LogProcessed("[Deleted]", path);
});
ProcessMovedAssets(movedFromPaths, movedAssets);
}
private static void ProcessAssets(string[] assets, Action<string> processor)
{
if (assets == null) return;
foreach (var asset in assets)
{
if (ShouldProcessAsset(asset))
{
processor?.Invoke(asset);
}
}
}
private static void ProcessMovedAssets(string[] oldPaths, string[] newPaths)
{
if (oldPaths == null || newPaths == null) return;
for (int i = 0; i < oldPaths.Length; i++)
{
if (ShouldProcessAsset(oldPaths[i]))
{
EditorSpriteSaveInfo.OnDeleteSprite(oldPaths[i]);
LogProcessed("[Moved From]", oldPaths[i]);
EditorSpriteSaveInfo.MarkParentAtlasesDirty(oldPaths[i]);
}
if (ShouldProcessAsset(newPaths[i]))
{
EditorSpriteSaveInfo.OnImportSprite(newPaths[i]);
LogProcessed("[Moved To]", newPaths[i]);
EditorSpriteSaveInfo.MarkParentAtlasesDirty(newPaths[i]);
}
}
}
private static bool ShouldProcessAsset(string assetPath)
{
var config = AtlasConfiguration.Instance;
if (string.IsNullOrEmpty(assetPath)) return false;
if (assetPath.StartsWith("Packages/")) return false;
if (!assetPath.StartsWith(config.sourceAtlasRoot)) return false;
if (assetPath.StartsWith(config.excludeFolder)) return false;
if (!IsValidImageFile(assetPath)) return false;
foreach (var keyword in config.excludeKeywords)
{
if (assetPath.IndexOf(keyword, StringComparison.OrdinalIgnoreCase) >= 0)
return false;
}
return true;
}
private static bool IsValidImageFile(string path)
{
var ext = Path.GetExtension(path).ToLower();
return ext switch
{
".png" => true,
".jpg" => true,
".jpeg" => true,
_ => false
};
}
private static void LogProcessed(string operation, string path)
{
if (AtlasConfiguration.Instance.enableLogging)
{
Debug.Log($"{operation} {Path.GetFileName(path)}\nPath: {path}");
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e5d969d76e1a36445810f61bafc4f7db
timeCreated: 1738748276

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 531cb4efe9b94a558716c7ef8a401f8b
timeCreated: 1705976900

View File

@@ -1,543 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEditor.U2D;
using UnityEngine;
using UnityEngine.U2D;
using Object = UnityEngine.Object;
/// <summary>
/// 图集导入管线。
/// </summary>
public class SpritePostprocessor : AssetPostprocessor
{
static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
{
foreach (var s in importedAssets)
{
EditorSpriteSaveInfo.OnImportSprite(s);
}
foreach (var s in deletedAssets)
{
EditorSpriteSaveInfo.OnDeleteSprite(s);
}
foreach (var s in movedFromAssetPaths)
{
EditorSpriteSaveInfo.OnDeleteSprite(s);
}
foreach (var s in movedAssets)
{
EditorSpriteSaveInfo.OnImportSprite(s);
}
}
}
public static class EditorSpriteSaveInfo
{
private const string NORMAL_ATLAS_DIR = "Assets/AssetArt/Atlas";
private const string UI_SPRITE_PATH = "Assets/AssetRaw/UIRaw";
private const string UI_ATLAS_PATH = "Assets/AssetRaw/UIRaw/Atlas";
private static readonly List<string> _dirtyAtlasList = new List<string>();
private static readonly Dictionary<string, List<string>> _allASprites = new Dictionary<string, List<string>>();
private static readonly Dictionary<string, string> _uiAtlasMap = new Dictionary<string, string>();
private static bool _isInit = false;
private static bool _dirty = false;
public static void Init()
{
if (_isInit)
{
return;
}
EditorApplication.update += CheckDirty;
//读取所有图集信息
string[] findAssets = AssetDatabase.FindAssets("t:SpriteAtlas", new[] { NORMAL_ATLAS_DIR });
foreach (var findAsset in findAssets)
{
var path = AssetDatabase.GUIDToAssetPath(findAsset);
SpriteAtlas sa = AssetDatabase.LoadAssetAtPath(path, typeof(SpriteAtlas)) as SpriteAtlas;
if (sa == null)
{
Debug.LogError($"加载图集数据{path}失败");
continue;
}
string atlasName = Path.GetFileNameWithoutExtension(path);
var objects = sa.GetPackables();
foreach (var o in objects)
{
if (!_allASprites.TryGetValue(atlasName, out var list))
{
list = new List<string>();
_allASprites.Add(atlasName, list);
}
list.Add(AssetDatabase.GetAssetPath(o));
}
}
_isInit = true;
}
public static void CheckDirty()
{
if (_dirty)
{
_dirty = false;
AssetDatabase.Refresh();
float lastProgress = -1;
for (int i = 0; i < _dirtyAtlasList.Count; i++)
{
string atlasName = _dirtyAtlasList[i];
Debug.Log("更新图集 : " + atlasName);
var curProgress = (float)i / _dirtyAtlasList.Count;
if (curProgress > lastProgress + 0.01f)
{
lastProgress = curProgress;
var progressText = $"当前进度:{i}/{_dirtyAtlasList.Count} {atlasName}";
bool cancel = EditorUtility.DisplayCancelableProgressBar("刷新图集" + atlasName, progressText, curProgress);
if (cancel)
{
break;
}
}
bool isUI = atlasName.StartsWith("UIRaw");
SaveAtlas(atlasName, isUI);
}
EditorUtility.ClearProgressBar();
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
_dirtyAtlasList.Clear();
}
}
public static void OnImportSprite(string assetPath)
{
if (!assetPath.StartsWith(UI_SPRITE_PATH))
{
return;
}
TextureImporter ti = AssetImporter.GetAtPath(assetPath) as TextureImporter;
if (ti != null)
{
var modify = false;
if (assetPath.StartsWith(UI_SPRITE_PATH))
{
if (ti.textureType != TextureImporterType.Sprite)
{
ti.textureType = TextureImporterType.Sprite;
modify = true;
}
if (!string.IsNullOrEmpty(ti.spritePackingTag))
{
ti.spritePackingTag = string.Empty;
modify = true;
}
var setting = new TextureImporterSettings();
ti.ReadTextureSettings(setting);
if (setting.spriteGenerateFallbackPhysicsShape)
{
setting.spriteGenerateFallbackPhysicsShape = false;
ti.SetTextureSettings(setting);
modify = true;
}
if (IsKeepRawImage(assetPath))
{
//调整android格式
var andPlatformSettings = ti.GetPlatformTextureSettings("Android");
if (!andPlatformSettings.overridden)
{
andPlatformSettings.overridden = true;
modify = true;
}
if (andPlatformSettings.format != TextureImporterFormat.ASTC_6x6)
{
andPlatformSettings.format = TextureImporterFormat.ASTC_6x6;
andPlatformSettings.compressionQuality = 50;
ti.SetPlatformTextureSettings(andPlatformSettings);
modify = true;
}
//调整ios格式
var iosPlatformSettings = ti.GetPlatformTextureSettings("iPhone");
if (!iosPlatformSettings.overridden)
{
iosPlatformSettings.overridden = true;
modify = true;
}
if (iosPlatformSettings.format != TextureImporterFormat.ASTC_5x5)
{
iosPlatformSettings.format = TextureImporterFormat.ASTC_5x5;
iosPlatformSettings.compressionQuality = 50;
ti.SetPlatformTextureSettings(iosPlatformSettings);
modify = true;
}
//调整WebGL格式
var webglSettings = ti.GetPlatformTextureSettings("WebGL");
if (!webglSettings.overridden)
{
webglSettings.overridden = true;
modify = true;
}
if (webglSettings.format != TextureImporterFormat.ASTC_6x6)
{
webglSettings.format = TextureImporterFormat.ASTC_6x6;
webglSettings.compressionQuality = 50;
ti.SetPlatformTextureSettings(webglSettings);
modify = true;
}
}
}
if (modify)
{
ti.SaveAndReimport();
}
if (ti.textureType == TextureImporterType.Sprite)
{
OnProcessSprite(assetPath);
}
}
}
/// <summary>
/// 是否保持散图(不打图集)
/// </summary>
/// <param name="dirPath"></param>
/// <returns></returns>
public static bool IsKeepRawImage(string dirPath)
{
return dirPath.Contains("UIRaw/Raw/") || dirPath.Contains("UIRaw_Raw_");
}
public static string GetSpritePath(string assetPath)
{
string path = assetPath.Substring(0, assetPath.LastIndexOf(".", StringComparison.Ordinal));
path = path.Replace("Assets/AssetRaw/", "");
return path;
}
/// <summary>
/// 根据文件路径,返回图集名称
/// </summary>
/// <param name="fullName"></param>
/// <returns></returns>
public static string GetPackageTag(string fullName)
{
fullName = fullName.Replace("\\", "/");
int idx = fullName.LastIndexOf("UIRaw", StringComparison.Ordinal);
if (idx == -1)
{
return "";
}
if (IsKeepRawImage(fullName))
{
return "";
}
var atlasPath = fullName.Substring(idx);
string str = atlasPath;
str = str.Substring(0, str.LastIndexOf("/", StringComparison.Ordinal)).Replace("/", "_");
return str;
}
public static void OnProcessSprite(string assetPath)
{
if (!assetPath.StartsWith("Assets"))
{
return;
}
if (assetPath.StartsWith("Assets/UIRaw_Delete"))
{
return;
}
Init();
var spriteName = Path.GetFileNameWithoutExtension(assetPath);
var spritePath = GetSpritePath(assetPath);
if (!_uiAtlasMap.TryGetValue(spriteName, out string oldAssetPath) || spritePath == oldAssetPath)
{
_uiAtlasMap[spriteName] = spritePath;
_dirty = true;
}
else
{
Debug.LogError($"有重名的图片:{spriteName}\n旧图集{oldAssetPath}\n新图集{spritePath} ");
_uiAtlasMap[spriteName] = spritePath;
_dirty = true;
}
string atlasName = GetPackageTag(assetPath);
if (string.IsNullOrEmpty(atlasName))
{
bool keepRaw = IsKeepRawImage(assetPath);
if (!keepRaw)
{
Debug.LogError($"empty packingTag of asset :{assetPath} !!!");
}
return;
}
else
{
if (!_allASprites.TryGetValue(atlasName, out var ret))
{
ret = new List<string>();
_allASprites.Add(atlasName, ret);
}
if (!ret.Contains(assetPath))
{
ret.Add(assetPath);
_dirty = true;
if (!_dirtyAtlasList.Contains(atlasName))
{
_dirtyAtlasList.Add(atlasName);
}
}
}
}
public static void OnDeleteSprite(string assetPath)
{
if (assetPath.StartsWith("Assets/UIRaw_Delete"))
{
return;
}
if (!assetPath.StartsWith(UI_SPRITE_PATH))
{
return;
}
Init();
string atlasName = GetPackageTag(assetPath);
if (!_allASprites.TryGetValue(atlasName, out var ret))
{
return;
}
if (!ret.Exists(s => Path.GetFileName(s) == Path.GetFileName(assetPath)))
{
return;
}
if (assetPath.StartsWith(UI_SPRITE_PATH))
{
var spriteName = Path.GetFileNameWithoutExtension(assetPath);
if (_uiAtlasMap.ContainsKey(spriteName))
{
_uiAtlasMap.Remove(spriteName);
_dirty = true;
}
}
ret.Remove(assetPath);
_dirty = true;
if (!_dirtyAtlasList.Contains(atlasName))
{
_dirtyAtlasList.Add(atlasName);
}
}
#region
public static void SaveAtlas(string atlasName, bool isUI)
{
List<Object> spriteList = new List<Object>();
if (_allASprites.TryGetValue(atlasName, out var list))
{
list.Sort(StringComparer.Ordinal);
foreach (var s in list)
{
var sprite = AssetDatabase.LoadAssetAtPath<Sprite>(s);
if (sprite != null)
{
spriteList.Add(sprite);
}
}
}
var path = $"{NORMAL_ATLAS_DIR}/{atlasName}.spriteatlas";
if (spriteList.Count == 0)
{
if (File.Exists(path))
{
AssetDatabase.DeleteAsset(path);
}
return;
}
var atlas = new SpriteAtlas();
// var atlas = new SpriteAtlasAsset();
var setting = new SpriteAtlasPackingSettings
{
blockOffset = 1,
padding = 2,
enableRotation = true
};
bool isOpaque = atlasName.Contains("Opaque");
var textureSetting = new SpriteAtlasTextureSettings
{
generateMipMaps = false,
sRGB = true,
filterMode = FilterMode.Bilinear
};
atlas.SetTextureSettings(textureSetting);
var iphonePlatformSetting = atlas.GetPlatformSettings("iPhone");
if (!iphonePlatformSetting.overridden)
{
iphonePlatformSetting.overridden = true;
iphonePlatformSetting.format = TextureImporterFormat.ASTC_5x5;
iphonePlatformSetting.compressionQuality = 100;
atlas.SetPlatformSettings(iphonePlatformSetting);
}
var androidPlatformSetting = atlas.GetPlatformSettings("Android");
if (!androidPlatformSetting.overridden)
{
androidPlatformSetting.overridden = true;
androidPlatformSetting.format = TextureImporterFormat.ASTC_6x6;
androidPlatformSetting.compressionQuality = 100;
atlas.SetPlatformSettings(androidPlatformSetting);
}
var webglSettings = atlas.GetPlatformSettings("WebGL");
if (!webglSettings.overridden)
{
webglSettings.overridden = true;
webglSettings.format = TextureImporterFormat.ASTC_6x6;
webglSettings.compressionQuality = 50;
atlas.SetPlatformSettings(webglSettings);
}
atlas.SetPackingSettings(setting);
atlas.Add(spriteList.ToArray());
AssetDatabase.CreateAsset(atlas, path);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
#endregion
#region
private static readonly Dictionary<string, List<string>> _tempAllASprites = new Dictionary<string, List<string>>();
[MenuItem("TEngine/Atlas/重新生成UI图集", false, 90)]
static void ForceGenAtlas()
{
Init();
List<string> needSaveAtlas = new List<string>();
_tempAllASprites.Clear();
_allASprites.Clear();
var findAssets = AssetDatabase.FindAssets("t:sprite", new[] { UI_ATLAS_PATH });
foreach (var findAsset in findAssets)
{
var path = AssetDatabase.GUIDToAssetPath(findAsset);
var atlasName = GetPackageTag(path);
if (!_tempAllASprites.TryGetValue(atlasName, out var spriteList))
{
spriteList = new List<string>();
_tempAllASprites[atlasName] = spriteList;
}
if (!spriteList.Contains(path))
{
spriteList.Add(path);
}
}
//有变化的才刷
var iter = _tempAllASprites.GetEnumerator();
while (iter.MoveNext())
{
bool needSave = false;
var atlasName = iter.Current.Key;
var newSpritesList = iter.Current.Value;
if (_allASprites.TryGetValue(atlasName, out var existSprites))
{
if (existSprites.Count != newSpritesList.Count)
{
needSave = true;
existSprites.Clear();
existSprites.AddRange(newSpritesList);
}
else
{
for (int i = 0; i < newSpritesList.Count; i++)
{
if (!existSprites.Contains(newSpritesList[i]))
{
needSave = true;
break;
}
}
if (needSave)
{
existSprites.Clear();
existSprites.AddRange(newSpritesList);
}
}
}
else
{
needSave = true;
_allASprites.Add(atlasName, new List<string>(newSpritesList));
}
if (needSave && !needSaveAtlas.Contains(atlasName))
{
needSaveAtlas.Add(atlasName);
}
}
iter.Dispose();
foreach (var atlas in needSaveAtlas)
{
Debug.LogFormat("Gen atlas:{0}", atlas);
SaveAtlas(atlas, true);
}
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
SpriteAtlasUtility.PackAllAtlases(EditorUserBuildSettings.activeBuildTarget);
Debug.Log("Gen end");
}
#endregion
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 0b37506bd00c3f14d83f966ae20ba8ec
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,31 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &1
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f529ff1e114e8ba4cb189f4233102cd5, type: 3}
m_Name:
m_EditorClassIdentifier:
outputAtlasDir: Assets/AssetArt/Atlas
sourceAtlasRoot: Assets/AssetRaw/UIRaw/Atlas
excludeFolder: Assets/AssetRaw/UIRaw/Raw
androidFormat: 50
iosFormat: 49
webglFormat: 50
padding: 4
enableRotation: 1
blockOffset: 1
tightPacking: 1
compressionQuality: 50
autoGenerate: 1
enableLogging: 1
enableV2: 1
excludeKeywords:
- _Delete
- _Temp