#if UNITY_EDITOR using System; using System.Collections.Generic; using System.IO; using System.Linq; using HotUpdate.UI; using UnityEditor; using UnityEditor.U2D; using UnityEngine; using UnityEngine.U2D; using Object = UnityEngine.Object; namespace AIGame.EditorTools { /// 移植 ClientTrunk SpriteAtalsGenerator 的按目录打图集规则,增量维护并保留 GUID。 public static class UiSpriteAtlasGenerator { public const string GeneratedLabel = "AIGame.UI.GeneratedAtlas"; private static bool s_Busy; [MenuItem("Tools/UI/Generate Sprite Atlases")] public static void GenerateFromMenu() { Debug.Log("[UIAtlas] " + GenerateAll()); } public static string GenerateAll() { if (s_Busy) return "Generation already in progress."; if (EditorApplication.isPlayingOrWillChangePlaymode) throw new InvalidOperationException("Generate UI atlases in Edit Mode."); s_Busy = true; try { var groups = CollectTextures(); EnsureFolder(UiAtlasPaths.AtlasRoot); // V1 matches the reference project and supports SpriteAtlas late binding in bundles. if (EditorSettings.spritePackerMode != SpritePackerMode.AlwaysOnAtlas) EditorSettings.spritePackerMode = SpritePackerMode.AlwaysOnAtlas; int changed = 0, deleted = 0; foreach (var group in groups) { string path = group.Key; var atlas = AssetDatabase.LoadAssetAtPath(path); bool created = atlas == null; if (created) { atlas = new SpriteAtlas(); AssetDatabase.CreateAsset(atlas, path); } bool dirty = Configure(atlas, group.Value.ToArray()) || created; var labels = AssetDatabase.GetLabels(atlas); if (!labels.Contains(GeneratedLabel)) { AssetDatabase.SetLabels(atlas, labels.Concat(new[] { GeneratedLabel }).ToArray()); dirty = true; } if (!dirty) continue; EditorUtility.SetDirty(atlas); AssetDatabase.SaveAssetIfDirty(atlas); changed++; } foreach (string guid in AssetDatabase.FindAssets("l:" + GeneratedLabel, new[] { UiAtlasPaths.AtlasRoot })) { string path = AssetDatabase.GUIDToAssetPath(guid); if (groups.ContainsKey(path) || !path.EndsWith(".spriteatlas", StringComparison.Ordinal)) continue; if (!AssetDatabase.DeleteAsset(path)) throw new IOException("Cannot remove stale UI atlas: " + path); deleted++; } return $"atlases={groups.Count}, textures={groups.Values.Sum(g => g.Count)}, updated={changed}, removed={deleted}"; } finally { s_Busy = false; } } public static SortedDictionary> CollectTextures() { var groups = new SortedDictionary>(StringComparer.Ordinal); var names = new Dictionary>(StringComparer.Ordinal); if (!AssetDatabase.IsValidFolder(UiAtlasPaths.TextureRoot)) return groups; foreach (string path in AssetDatabase.FindAssets("t:Texture2D", new[] { UiAtlasPaths.TextureRoot }) .Select(AssetDatabase.GUIDToAssetPath).OrderBy(p => p, StringComparer.Ordinal)) { var importer = AssetImporter.GetAtPath(path) as TextureImporter; if (importer == null || importer.textureType != TextureImporterType.Sprite) throw new InvalidOperationException("UI atlas input must be a Sprite: " + path); string atlasPath = UiAtlasPaths.GetAtlasPathForTexture(path); if (!groups.TryGetValue(atlasPath, out var textures)) { groups.Add(atlasPath, textures = new List()); names.Add(atlasPath, new HashSet(StringComparer.Ordinal)); } var sprites = AssetDatabase.LoadAllAssetsAtPath(path).OfType().ToArray(); if (sprites.Length == 0) throw new InvalidOperationException("UI texture has no sprites: " + path); foreach (var sprite in sprites) if (!names[atlasPath].Add(sprite.name)) throw new InvalidOperationException($"Duplicate sprite name '{sprite.name}' in {atlasPath}: {path}"); textures.Add(AssetDatabase.LoadAssetAtPath(path)); } // Names are also YooAsset addresses; reject case-only collisions before writing assets. if (groups.Keys.Distinct(StringComparer.OrdinalIgnoreCase).Count() != groups.Count) throw new InvalidOperationException("UI atlas folder names differ only by case."); return groups; } private static bool Configure(SpriteAtlas atlas, Object[] textures) { bool changed = false; if (!atlas.GetPackables().SequenceEqual(textures)) { atlas.Remove(atlas.GetPackables()); atlas.Add(textures); changed = true; } var packing = atlas.GetPackingSettings(); var desiredPacking = packing; desiredPacking.enableRotation = false; desiredPacking.enableTightPacking = false; desiredPacking.enableAlphaDilation = true; desiredPacking.padding = 4; if (!packing.Equals(desiredPacking)) { atlas.SetPackingSettings(desiredPacking); changed = true; } var settings = atlas.GetTextureSettings(); var desiredSettings = settings; desiredSettings.readable = false; desiredSettings.generateMipMaps = false; desiredSettings.sRGB = true; desiredSettings.filterMode = FilterMode.Bilinear; if (!settings.Equals(desiredSettings)) { atlas.SetTextureSettings(desiredSettings); changed = true; } // SBP 的 PackAllAtlases 需要启用此标志,才能建立打包纹理关联并剥离原图像素。 // 资源仍由 YooAsset 收集到对应图集 bundle;运行时加载器保留延迟绑定兼容。 if (!atlas.IsIncludeInBuild()) { atlas.SetIncludeInBuild(true); changed = true; } changed |= SetPlatform(atlas, "DefaultTexturePlatform", TextureImporterFormat.Automatic, false); changed |= SetPlatform(atlas, "Standalone", TextureImporterFormat.DXT5, true); changed |= SetPlatform(atlas, "iPhone", TextureImporterFormat.ASTC_6x6, true); changed |= SetPlatform(atlas, "Android", TextureImporterFormat.ASTC_6x6, true); changed |= SetPlatform(atlas, "WebGL", TextureImporterFormat.ASTC_8x8, true); return changed; } private static bool SetPlatform(SpriteAtlas atlas, string platform, TextureImporterFormat format, bool overridden) { var settings = atlas.GetPlatformSettings(platform); if (settings.maxTextureSize == 2048 && settings.format == format && settings.overridden == overridden && !settings.crunchedCompression && settings.compressionQuality == 100 && settings.textureCompression == TextureImporterCompression.CompressedHQ) return false; settings.name = platform; settings.overridden = overridden; settings.maxTextureSize = 2048; settings.format = format; settings.textureCompression = TextureImporterCompression.CompressedHQ; settings.compressionQuality = 100; settings.crunchedCompression = false; atlas.SetPlatformSettings(settings); return true; } private static void EnsureFolder(string path) { if (AssetDatabase.IsValidFolder(path)) return; string parent = Path.GetDirectoryName(path).Replace('\\', '/'); EnsureFolder(parent); AssetDatabase.CreateFolder(parent, Path.GetFileName(path)); } } [InitializeOnLoad] public sealed class UiSpriteAtlasPostprocessor : AssetPostprocessor { private static bool s_Pending; static UiSpriteAtlasPostprocessor() { Schedule(); } private void OnPreprocessTexture() { if (!UiAtlasPaths.IsTexturePath(assetPath)) return; var importer = (TextureImporter)assetImporter; importer.textureType = TextureImporterType.Sprite; if (importer.spriteImportMode == SpriteImportMode.None) importer.spriteImportMode = SpriteImportMode.Single; importer.mipmapEnabled = false; importer.alphaIsTransparency = true; importer.npotScale = TextureImporterNPOTScale.None; } private static void OnPostprocessAllAssets(string[] imported, string[] deleted, string[] moved, string[] movedFrom) { if (imported.Concat(deleted).Concat(moved).Concat(movedFrom) .Any(p => p == UiAtlasPaths.TextureRoot || UiAtlasPaths.IsTexturePath(p))) Schedule(); } private static void Schedule() { if (s_Pending) return; s_Pending = true; EditorApplication.update += UpdateAtlases; } private static void UpdateAtlases() { if (EditorApplication.isCompiling || EditorApplication.isUpdating || EditorApplication.isPlayingOrWillChangePlaymode || BuildPipeline.isBuildingPlayer) return; s_Pending = false; EditorApplication.update -= UpdateAtlases; try { UiSpriteAtlasGenerator.GenerateAll(); } catch (Exception ex) { Debug.LogException(ex); } } } } #endif