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();
}
}
}