Last active
April 23, 2025 10:35
-
-
Save AliAlbarrak/e3f66632d3b4f7c3fccbcd750dab1e4b to your computer and use it in GitHub Desktop.
Auto generate Unity's SpriteAtlas per scene and per asset bundle [WIP]
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System.Collections.Generic; | |
| using System.IO; | |
| using System.Linq; | |
| using UnityEditor; | |
| using UnityEditor.AddressableAssets; | |
| using UnityEditor.AddressableAssets.Settings; | |
| using UnityEditor.AddressableAssets.Settings.GroupSchemas; | |
| using UnityEditor.U2D; | |
| using UnityEngine; | |
| using UnityEngine.U2D; | |
| public static class SpriteAtlasGenerator | |
| { | |
| private const string AtlasFolder = "Assets/Sprites/Sprite Atlas"; | |
| private const int GroupingThreshold = 20; | |
| private const string TemplateAtlasName = "Template"; | |
| private const string AtlasAddressableGroupName = "Sprite Atlases"; | |
| [MenuItem("Tools/Kammelna/Generate Sprite Atlases", priority = 1)] | |
| private static void GenerateSpriteAtlas() | |
| { | |
| EnsureAtlasFolderExist(); | |
| var bundleGroups = GetBundlesTextureGroups(); | |
| var scenesGroups = GetScenesTextureGroups(); | |
| var commonDependencies = ExtractCommonDependencies(bundleGroups, scenesGroups); | |
| CreateAtlases(scenesGroups, "Scenes"); | |
| CreateAtlases(bundleGroups, "Bundles", true); | |
| CreateCommonAtlases(commonDependencies); | |
| AssetDatabase.Refresh(); | |
| AddressableAssetSettingsDefaultObject.Settings.SetDirty(AddressableAssetSettings.ModificationEvent.BatchModification, null, true, true); | |
| SpriteAtlasUtility.PackAllAtlases(EditorUserBuildSettings.activeBuildTarget); | |
| } | |
| private static void CreateAtlases(Dictionary<List<string>, List<string>> scenesGroups, string groupName, bool markAddressable = false) | |
| { | |
| var alphaGroup = new List<Object>(); | |
| var template = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(GetAtlasPath(TemplateAtlasName)); | |
| foreach (var pair in scenesGroups) | |
| { | |
| var textures = new List<Object>(); | |
| foreach (var texturePath in pair.Value) | |
| { | |
| var texture = AssetDatabase.LoadAssetAtPath<Texture2D>(texturePath); | |
| if (texture.isReadable) | |
| { | |
| alphaGroup.Add(texture); | |
| continue; | |
| } | |
| textures.Add(texture); | |
| } | |
| if (!textures.Any()) | |
| continue; | |
| var name = pair.Key.Count > 3 ? $"Multiple {groupName} ({pair.Key.Count})" : string.Join("_", pair.Key); | |
| CreateAtlas(template, textures, name, markAddressable); | |
| } | |
| CreateAtlas(template, alphaGroup, $"Read Write Enabled Sprites ({groupName})", markAddressable, true); | |
| } | |
| private static void CreateCommonAtlases(List<string> commonDependencies) | |
| { | |
| var template = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(GetAtlasPath(TemplateAtlasName)); | |
| var textures = commonDependencies.Select(AssetDatabase.LoadAssetAtPath<Texture>).ToList(); | |
| var normalTextures = textures.Where(texture => !texture.isReadable).Cast<Object>().ToList(); | |
| var alphaTextures = textures.Where(texture => texture.isReadable).Cast<Object>().ToList(); | |
| CreateAtlas(template, normalTextures, "Scenes Bundle Commons", true); | |
| CreateAtlas(template, alphaTextures, "Scenes Bundle Commons Read Write Enabled", true, true); | |
| } | |
| private static void CreateAtlas(SpriteAtlas template, List<Object> textures, string atlasName, bool markAddressable = false, bool readable = false) | |
| { | |
| var alphaAtlas = Object.Instantiate(template); | |
| if(readable) | |
| { | |
| var settings = alphaAtlas.GetTextureSettings(); | |
| settings.readable = true; | |
| alphaAtlas.SetTextureSettings(settings); | |
| } | |
| alphaAtlas.Add(textures.ToArray()); | |
| var atlasPath = AssetDatabase.GenerateUniqueAssetPath(GetAtlasPath(atlasName)); | |
| AssetDatabase.CreateAsset(alphaAtlas, atlasPath); | |
| if (markAddressable) | |
| { | |
| var settings = AddressableAssetSettingsDefaultObject.Settings; | |
| var assetGroup = settings.groups.FirstOrDefault(group => group.Name == AtlasAddressableGroupName); | |
| if(assetGroup == null) | |
| assetGroup = settings.CreateGroup(AtlasAddressableGroupName, false, false, false, null, typeof(BundledAssetGroupSchema), typeof(ContentUpdateGroupSchema)); | |
| assetGroup.GetSchema<ContentUpdateGroupSchema>().StaticContent = true; | |
| settings.CreateOrMoveEntry(AssetDatabase.AssetPathToGUID(atlasPath), assetGroup, false, false); | |
| } | |
| } | |
| private static List<string> ExtractCommonDependencies(Dictionary<List<string>,List<string>> bundleGroups, Dictionary<List<string>,List<string>> scenesGroups) | |
| { | |
| var bundleDependencies = bundleGroups.SelectMany(pair => pair.Value); | |
| var scenesDependencies = scenesGroups.SelectMany(pair => pair.Value); | |
| var commonDependencies = bundleDependencies.Intersect(scenesDependencies).ToList(); | |
| foreach (var pair in bundleGroups) | |
| pair.Value.RemoveList(commonDependencies); | |
| foreach (var pair in scenesGroups) | |
| pair.Value.RemoveList(commonDependencies); | |
| return commonDependencies; | |
| } | |
| private static string GetAtlasPath(string name) | |
| { | |
| return $"{AtlasFolder}/{name}.spriteatlas"; | |
| } | |
| private static void EnsureAtlasFolderExist() | |
| { | |
| if (!AssetDatabase.IsValidFolder(AtlasFolder)) | |
| { | |
| var split = AtlasFolder.LastIndexOf('/'); | |
| var path = AtlasFolder.Substring(0, split); | |
| var name = AtlasFolder.Substring(split + 1); | |
| AssetDatabase.CreateFolder(path, name); | |
| AssetDatabase.Refresh(); | |
| } | |
| } | |
| private static Dictionary<List<string>, List<string>> GetBundlesTextureGroups() | |
| { | |
| Dictionary<string, List<string>> assetsByDependent = new Dictionary<string, List<string>>(); | |
| var groupsList = new List<string>(); | |
| foreach (var assetGroup in AddressableAssetSettingsDefaultObject.Settings.groups.Where(group => group.GetSchema<ContentUpdateGroupSchema>() != null)) | |
| { | |
| foreach (var assetEntry in assetGroup.entries) | |
| { | |
| if (AssetDatabase.GetMainAssetTypeAtPath(assetEntry.AssetPath) != typeof(GameObject)) | |
| continue; | |
| groupsList.Add(assetGroup.Name); | |
| AddToDependencies(assetsByDependent, assetEntry.AssetPath, assetGroup.Name); | |
| } | |
| } | |
| groupsList = groupsList.Distinct().ToList(); | |
| return ComputeTextureGroups(assetsByDependent, groupsList); | |
| } | |
| private static Dictionary<List<string>, List<string>> GetScenesTextureGroups() | |
| { | |
| Dictionary<string, List<string>> assetsByDependent = new Dictionary<string, List<string>>(); | |
| var scenesList = new List<string>(); | |
| foreach (var scene in EditorBuildSettings.scenes.Where(scene => scene.enabled)) | |
| { | |
| var sceneName = Path.GetFileNameWithoutExtension(scene.path); | |
| scenesList.Add(sceneName); | |
| AddToDependencies(assetsByDependent, scene.path, sceneName); | |
| } | |
| return ComputeTextureGroups(assetsByDependent, scenesList); | |
| } | |
| private static Dictionary<List<string>, List<string>> ComputeTextureGroups(Dictionary<string, List<string>> assetsByDependent, List<string> groupsList) | |
| { | |
| var textureGroups = assetsByDependent.GroupBy(pair => pair.Value, new StringListComparer()) | |
| .OrderBy(group => group.Select(pair => pair.Key).Count()) | |
| .ToDictionary(group => group.Key, group => group.Select(pair => pair.Key).ToList()); | |
| if (!textureGroups.ContainsKey(groupsList)) | |
| textureGroups[groupsList] = new List<string>(); | |
| while (textureGroups.Min(pair => pair.Value.Count) < GroupingThreshold) | |
| { | |
| var minGroup = textureGroups.Where(pair => pair.Key.Count > 2 && pair.Value.Count < GroupingThreshold).MinBy(pair => pair.Key.Count); | |
| if (minGroup.Key.Count == groupsList.Count) | |
| break; | |
| var newGroup = textureGroups.Where(pair => pair.Key.Count > minGroup.Key.Count && !minGroup.Key.Except(pair.Key).Any()).MinBy(pair => pair.Key.Count); | |
| textureGroups.Remove(minGroup.Key); | |
| newGroup.Value.AddRange(minGroup.Value); | |
| textureGroups[newGroup.Key] = newGroup.Value; | |
| } | |
| return textureGroups; | |
| } | |
| private static void AddToDependencies(IDictionary<string, List<string>> assetsByDependent, string path, string dependentName) | |
| { | |
| string[] dependencies = AssetDatabase.GetDependencies(path); | |
| foreach (var dependencyPath in dependencies) | |
| { | |
| if (!dependencyPath.StartsWith("Assets/Sprites") || dependencyPath.Contains("/Resources/")) | |
| continue; | |
| if (AssetDatabase.GetMainAssetTypeAtPath(dependencyPath) != typeof(Texture2D)) | |
| continue; | |
| if (!assetsByDependent.ContainsKey(dependencyPath)) | |
| assetsByDependent[dependencyPath] = new List<string>(); | |
| if(!assetsByDependent[dependencyPath].Contains(dependentName)) | |
| assetsByDependent[dependencyPath].Add(dependentName); | |
| } | |
| } | |
| private class StringListComparer : IEqualityComparer<List<string>> | |
| { | |
| public bool Equals(List<string> x, List<string> y) | |
| { | |
| if (x is null || y is null) | |
| return x is null && y is null; | |
| return x.All(y.Contains); | |
| } | |
| public int GetHashCode(List<string> obj) | |
| { | |
| return 0; | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment