207 lines
10 KiB
C#
207 lines
10 KiB
C#
#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
|
|
{
|
|
/// <summary>移植 ClientTrunk SpriteAtalsGenerator 的按目录打图集规则,增量维护并保留 GUID。</summary>
|
|
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<SpriteAtlas>(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<string, List<Object>> CollectTextures()
|
|
{
|
|
var groups = new SortedDictionary<string, List<Object>>(StringComparer.Ordinal);
|
|
var names = new Dictionary<string, HashSet<string>>(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<Object>());
|
|
names.Add(atlasPath, new HashSet<string>(StringComparer.Ordinal));
|
|
}
|
|
var sprites = AssetDatabase.LoadAllAssetsAtPath(path).OfType<Sprite>().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<Texture2D>(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
|