Ship the WeChat hot-update catalogs and the camp, TideWhale T2, UI atlas, and animation-lab work.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.U2D;
|
||||
using UnityEngine.UI;
|
||||
using YooAsset;
|
||||
|
||||
namespace HotUpdate.UI
|
||||
{
|
||||
/// <summary>移植 AtlasManager 的延迟绑定;所有平台异步加载,句柄随 UI 生命周期释放。</summary>
|
||||
public sealed class UiAtlasLoader : MonoBehaviour
|
||||
{
|
||||
private sealed class Entry
|
||||
{
|
||||
public bool Loading = true;
|
||||
public SpriteAtlas Atlas;
|
||||
public AssetHandle Handle;
|
||||
}
|
||||
|
||||
private readonly Dictionary<string, Entry> _atlases = new Dictionary<string, Entry>(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, Sprite> _sprites = new Dictionary<string, Sprite>(StringComparer.Ordinal);
|
||||
private readonly Dictionary<Image, object> _imageRequests = new Dictionary<Image, object>();
|
||||
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<SpriteAtlas> 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<SpriteAtlas> onLoaded)
|
||||
{
|
||||
return LoadAtPathAsync(UiAtlasPaths.GetAtlasPath(relativeFolder), onLoaded);
|
||||
}
|
||||
|
||||
private IEnumerator LoadAtPathAsync(string path, Action<SpriteAtlas> 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<ResourcePackage>();
|
||||
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<SpriteAtlas>(path);
|
||||
yield return entry.Handle;
|
||||
if (_disposed) yield break;
|
||||
if (entry.Handle.Status == EOperationStatus.Succeeded)
|
||||
{
|
||||
entry.Atlas = entry.Handle.GetAssetObject<SpriteAtlas>();
|
||||
if (entry.Atlas != null) break;
|
||||
}
|
||||
entry.Handle.Release();
|
||||
entry.Handle = null;
|
||||
}
|
||||
#if UNITY_EDITOR
|
||||
if (entry.Atlas == null && _allowEditorFallback)
|
||||
entry.Atlas = UnityEditor.AssetDatabase.LoadAssetAtPath<SpriteAtlas>(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<Sprite> 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<Sprite> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user