using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.U2D; using UnityEngine.UI; using YooAsset; namespace HotUpdate.UI { /// 移植 AtlasManager 的延迟绑定;所有平台异步加载,句柄随 UI 生命周期释放。 public sealed class UiAtlasLoader : MonoBehaviour { private sealed class Entry { public bool Loading = true; public SpriteAtlas Atlas; public AssetHandle Handle; } private readonly Dictionary _atlases = new Dictionary(StringComparer.Ordinal); private readonly Dictionary _sprites = new Dictionary(StringComparer.Ordinal); private readonly Dictionary _imageRequests = new Dictionary(); private ResourcePackage _preferredPackage; private bool _allowEditorFallback = true; private bool _disposed; public void Init(ResourcePackage preferredPackage = null, bool allowEditorFallback = true) { _preferredPackage = preferredPackage; _allowEditorFallback = allowEditorFallback; } private void Awake() { SpriteAtlasManager.atlasRequested += RequestAtlas; } private void RequestAtlas(string atlasName, Action callback) { // 只接管本项目的 UI 图集,不拦截字体或第三方图集。 if (string.IsNullOrEmpty(atlasName) || !atlasName.StartsWith(UiAtlasPaths.AtlasNamePrefix, StringComparison.Ordinal) || !atlasName.EndsWith("Atlas", StringComparison.Ordinal) || atlasName.IndexOfAny(new[] { '/', '\\' }) >= 0) return; StartCoroutine(LoadAtPathAsync(UiAtlasPaths.AtlasRoot + "/" + atlasName + ".spriteatlas", callback)); } public IEnumerator LoadAtlasAsync(string relativeFolder, Action onLoaded) { return LoadAtPathAsync(UiAtlasPaths.GetAtlasPath(relativeFolder), onLoaded); } private IEnumerator LoadAtPathAsync(string path, Action onLoaded) { if (_disposed) { onLoaded?.Invoke(null); yield break; } if (_atlases.TryGetValue(path, out var cached)) { while (cached.Loading && !_disposed) yield return null; if (!_disposed) onLoaded?.Invoke(cached.Atlas); yield break; } var entry = new Entry(); _atlases.Add(path, entry); var packages = new List(); if (_preferredPackage != null) packages.Add(_preferredPackage); if (YooAssets.IsInitialized) foreach (string name in new[] { "Art", "DefaultPackage" }) if (YooAssets.TryGetPackage(name, out var package) && !packages.Contains(package)) packages.Add(package); try { foreach (var package in packages) { if (package.InitializeStatus != EOperationStatus.Succeeded || !package.IsLocationValid(path)) continue; entry.Handle = package.LoadAssetAsync(path); yield return entry.Handle; if (_disposed) yield break; if (entry.Handle.Status == EOperationStatus.Succeeded) { entry.Atlas = entry.Handle.GetAssetObject(); if (entry.Atlas != null) break; } entry.Handle.Release(); entry.Handle = null; } #if UNITY_EDITOR if (entry.Atlas == null && _allowEditorFallback) entry.Atlas = UnityEditor.AssetDatabase.LoadAssetAtPath(path); #endif } finally { entry.Loading = false; // 失败不留缓存,允许资源包就绪后重试。 if (entry.Atlas == null && !_disposed) { entry.Handle?.Release(); entry.Handle = null; _atlases.Remove(path); } } if (_disposed) yield break; if (entry.Atlas == null) Debug.LogWarning("[UIAtlas] Atlas unavailable: " + path); onLoaded?.Invoke(entry.Atlas); } public IEnumerator WaitForPending() { while (!_disposed) { bool pending = false; foreach (var entry in _atlases.Values) pending |= entry.Loading; if (!pending) yield break; yield return null; } } public IEnumerator LoadSpriteAsync(string relativeFolder, string spriteName, Action onLoaded) { if (string.IsNullOrEmpty(spriteName) || _disposed) { onLoaded?.Invoke(null); yield break; } string key = UiAtlasPaths.GetAtlasPath(relativeFolder) + "|" + spriteName; if (_sprites.TryGetValue(key, out var sprite) && sprite != null) { onLoaded?.Invoke(sprite); yield break; } SpriteAtlas atlas = null; yield return LoadAtlasAsync(relativeFolder, loaded => atlas = loaded); if (_disposed) yield break; if (atlas != null) { // GetSprite 创建的克隆需缓存并在销毁时释放。 if (!_sprites.TryGetValue(key, out sprite) || sprite == null) { sprite = atlas.GetSprite(spriteName); if (sprite != null) _sprites[key] = sprite; } } if (sprite == null) Debug.LogWarning($"[UIAtlas] Sprite unavailable: {relativeFolder}/{spriteName}"); onLoaded?.Invoke(sprite); } public IEnumerator SetSpriteAsync(Image image, string relativeFolder, string spriteName, Action onLoaded = null) { if (image == null || _disposed) yield break; var request = new object(); _imageRequests[image] = request; Sprite sprite = null; yield return LoadSpriteAsync(relativeFolder, spriteName, loaded => sprite = loaded); // 同一个 Image 后来的请求优先,旧请求不能覆盖新图片。 if (_disposed || !_imageRequests.TryGetValue(image, out var current) || current != request) yield break; _imageRequests.Remove(image); if (image == null) yield break; image.sprite = sprite; onLoaded?.Invoke(sprite); } private void OnDestroy() { _disposed = true; SpriteAtlasManager.atlasRequested -= RequestAtlas; StopAllCoroutines(); foreach (var sprite in _sprites.Values) if (sprite != null) Destroy(sprite); _sprites.Clear(); foreach (var entry in _atlases.Values) entry.Handle?.Release(); _atlases.Clear(); _imageRequests.Clear(); } } }