Files
test/Assets/PerfectWorld/Scripts/Addressable/AddressableManager.cs
T

529 lines
20 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.AddressableAssets.ResourceLocators;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.U2D;
namespace BrewMonster.Scripts
{
public class AddressableManager : MonoSingleton<AddressableManager>
{
private bool _isInitialized = false;
private UniTaskCompletionSource _initializationTcs;
private Dictionary<string, AsyncOperationHandle<GameObject>> _loadedPrefabAssets = new();
private Dictionary<string, AsyncOperationHandle<TextAsset>> _loadedTextAssets = new();
private Dictionary<string, AsyncOperationHandle<AudioClip>> _loadedAudioAssets = new();
private Dictionary<string, AsyncOperationHandle<Sprite>> _loadedSpriteAssets = new();
private Dictionary<string, AsyncOperationHandle<SpriteAtlas>> _loadedSpriteAtlasAssets = new();
private Dictionary<string, int> _loadedAssetReferenceCount = new();
/// <summary>
/// Whenever we release an asset, we store the timestamp of the release.
/// After a certain amount of time, we will release the asset from the cache.
/// </summary>
private Dictionary<string, float> _releaseAssetTimestamps = new();
[SerializeField]private float _releaseAssetTimeout = 10f;
public event Action OnDispose;
/// <summary>Get the count of currently loaded assets.</summary>
public int LoadedPrefabCount => _loadedPrefabAssets.Count;
public bool IsInitialized() => _isInitialized;
/// <summary>
/// Returns immediately if already initialized; otherwise waits for the
/// Addressables initialization callback without per-frame polling.
/// </summary>
public UniTask WaitUntilInitializedAsync()
{
if (_isInitialized) return UniTask.CompletedTask;
return _initializationTcs.Task;
}
protected override void Initialize()
{
base.Initialize();
_isInitialized = false;
_initializationTcs = new UniTaskCompletionSource();
Addressables.InitializeAsync().Completed += OnInitializeComplete;
}
#region Unity lifecycle
private List<string> _assetToForceRelease = new();
private void Update()
{
foreach (var kvp in _releaseAssetTimestamps)
{
if (Time.realtimeSinceStartup - kvp.Value > _releaseAssetTimeout)
{
ForceReleaseAsset(kvp.Key);
_assetToForceRelease.Add(kvp.Key);
}
}
if (_assetToForceRelease.Count > 0)
{
for (int i = 0; i < _assetToForceRelease.Count; i++)
{
ForceReleaseAsset(_assetToForceRelease[i]);
}
_assetToForceRelease.Clear();
}
}
#endregion
#region private functions
private void OnInitializeComplete(AsyncOperationHandle<IResourceLocator> handle)
{
if (handle.Status == AsyncOperationStatus.Succeeded)
{
_isInitialized = true;
_initializationTcs.TrySetResult();
BMLogger.Log($"AddressableManager: Initialized");
}
else
{
// print out the error
BMLogger.LogError($"AddressableManager: Failed to initialize: {handle.OperationException?.Message} {handle.OperationException?.StackTrace}");
}
}
private void RemoveFromReleaseAssetDictionary(string assetPath)
{
if (_releaseAssetTimestamps.ContainsKey(assetPath))
{
_releaseAssetTimestamps.Remove(assetPath);
}
}
private void IncreaseReferenceCount(string assetPath)
{
if (!_loadedAssetReferenceCount.ContainsKey(assetPath))
{
_loadedAssetReferenceCount[assetPath] = 0;
}
_loadedAssetReferenceCount[assetPath]++;
}
private void DecreaseReferenceCount(string assetPath)
{
if (_loadedAssetReferenceCount.TryGetValue(assetPath, out int count))
{
count--;
if (count <= 0)
{
_loadedAssetReferenceCount.Remove(assetPath);
// put into the release asset timestamp dictionary.
_releaseAssetTimestamps[assetPath] = Time.realtimeSinceStartup;
}
}
}
#endregion
#region public functions
AsyncOperationHandle<TextAsset> _loadedTextAssetHandle;
/// <summary>
/// Load a text asset asynchronously.
/// NOTE: The key must match the Addressables "Address" (or another valid key like a label/GUID).
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="assetPath"></param>
/// <returns></returns>
public async Task<TextAsset> LoadTextAssetAsync(string assetPath)
{
// increase the reference count of the asset.
if (!_loadedAssetReferenceCount.ContainsKey(assetPath))
{
_loadedAssetReferenceCount[assetPath] = 0;
}
_loadedAssetReferenceCount[assetPath]++;
// remove the asset from the release timestamp dictionary. So it won't be released.
RemoveFromReleaseAssetDictionary(assetPath);
if (_loadedTextAssets.TryGetValue(assetPath, out _loadedTextAssetHandle))
{
if (_loadedTextAssetHandle.IsValid() && _loadedTextAssetHandle.Result != null)
{
BMLogger.Log($"AddressableManager: Loaded text asset from cache: {assetPath}");
return _loadedTextAssetHandle.Result;
}
else
{
BMLogger.Log($"AddressableManager: Text asset handle is invalid or result is null, need to load new one: {assetPath}");
}
}
try
{
var handle = Addressables.LoadAssetAsync<TextAsset>(assetPath);
await handle.Task;
_loadedTextAssets[assetPath] = handle;
return handle.Result;
}
catch (Exception e)
{
BMLogger.LogError($"AddressableManager: Failed to load TextAsset '{assetPath}': {e}");
return null;
}
}
AsyncOperationHandle<SpriteAtlas> _loadedSpriteAtlasHandle;
/// <summary>Load a sprite atlas asynchronously.</summary>
public async Task<SpriteAtlas> LoadSpriteAtlasAsync(string assetPath)
{
// increase the reference count of the asset.
if (!_loadedAssetReferenceCount.ContainsKey(assetPath))
{
_loadedAssetReferenceCount[assetPath] = 0;
}
_loadedAssetReferenceCount[assetPath]++;
// remove the asset from the release timestamp dictionary. So it won't be released.
RemoveFromReleaseAssetDictionary(assetPath);
if (_loadedSpriteAtlasAssets.TryGetValue(assetPath, out _loadedSpriteAtlasHandle))
{
if (_loadedTextAssetHandle.IsValid() && _loadedTextAssetHandle.Result != null)
{
BMLogger.Log($"AddressableManager: Loaded text asset from cache: {assetPath}");
return _loadedSpriteAtlasHandle.Result;
}
else
{
BMLogger.Log($"AddressableManager: Text asset handle is invalid or result is null, need to load new one: {assetPath}");
}
}
try
{
var handle = Addressables.LoadAssetAsync<SpriteAtlas>(assetPath);
await handle.Task;
if (handle.OperationException != null)
{
BMLogger.Log($"AddressableManager: Failed to load Sprite Atlas '{assetPath}': {handle.OperationException.Message} {handle.OperationException.StackTrace}");
#if UNITY_EDITOR
_invalidAssetPaths.Add(assetPath);
#endif
return null;
}
_loadedSpriteAtlasAssets[assetPath] = handle;
return handle.Result;
}
catch (Exception e)
{
BMLogger.LogError($"AddressableManager: Failed to load SpriteAtlas '{assetPath}': {e}");
return null;
}
}
AsyncOperationHandle<GameObject> _loadedPrefabHandle;
/// <summary>
/// Load an asset asynchronously. The address should look like this: "models/npcs/npc/魅灵首领/魅灵首领/魅灵首领.prefab"
/// </summary>
/// <returns></returns>
public async Task<GameObject> LoadPrefabAsync(string assetPath)
{
// remove the asset from the release timestamp dictionary. So it won't be released.
RemoveFromReleaseAssetDictionary(assetPath);
if (!KeyExists(assetPath, typeof(GameObject)))
{
BMLogger.LogWarning($"AddressableManager: Prefab '{assetPath}' does not exist");
return null;
}
if (_loadedPrefabAssets.TryGetValue(assetPath, out _loadedPrefabHandle))
{
if (_loadedPrefabHandle.IsValid() && _loadedPrefabHandle.Result != null)
{
// BMLogger.Log($"AddressableManager: Loaded prefab from cache: {assetPath}");
return _loadedPrefabHandle.Result;
}
else
{
// BMLogger.Log($"AddressableManager: Prefab handle is invalid or result is null, need to load new one: {assetPath}");
}
}
try
{
var handle = Addressables.LoadAssetAsync<GameObject>(assetPath);
await handle.Task;
if (handle.OperationException != null)
{
BMLogger.Log($"AddressableManager: Failed to load Prefab '{assetPath}': {handle.OperationException.Message} {handle.OperationException.StackTrace}");
#if UNITY_EDITOR
_invalidAssetPaths.Add(assetPath);
#endif
return null;
}
_loadedPrefabAssets[assetPath] = handle;
return handle.Result;
}
catch (System.Exception e)
{
BMLogger.Log($"AddressableManager: Failed to load Prefab '{assetPath}': {e.Message} {e.StackTrace}");
#if UNITY_EDITOR
_invalidAssetPaths.Add(assetPath);
#endif
return null;
}
}
AsyncOperationHandle<AudioClip> _loadedAudioClipHandle;
/// <summary>
/// Load an AudioClip asynchronously (e.g. skill SFX). Key must match the Addressables address.
/// </summary>
public async Task<AudioClip> LoadAudioClipAsync(string assetPath)
{
// BMLogger.LogError($"HoangDEv : AddressableManager: LoadAudioClipAsync called with assetPath: {assetPath}");
RemoveFromReleaseAssetDictionary(assetPath);
if (_loadedAudioAssets.TryGetValue(assetPath, out _loadedAudioClipHandle))
{
if (_loadedAudioClipHandle.IsValid() && _loadedAudioClipHandle.Result != null)
{
BMLogger.Log($"AddressableManager: Loaded audio from cache: {assetPath}");
return _loadedAudioClipHandle.Result;
}
}
try
{
var handle = Addressables.LoadAssetAsync<AudioClip>(assetPath.Trim());
await handle.Task;
if (handle.OperationException != null)
{
BMLogger.Log($"AddressableManager: Failed to load AudioClip '{assetPath}': {handle.OperationException.Message}");
return null;
}
_loadedAudioAssets[assetPath] = handle;
return handle.Result;
}
catch (Exception e)
{
BMLogger.LogError($"AddressableManager: Failed to load AudioClip '{assetPath}': {e.Message}");
return null;
}
}
AsyncOperationHandle<Sprite> _loadedSpriteHandle;
/// <summary>
/// Load a Sprite asynchronously (e.g. UI art). Key must match the Addressables address.
/// </summary>
public async Task<Sprite> LoadSpriteAsync(string assetPath)
{
RemoveFromReleaseAssetDictionary(assetPath);
if (_loadedSpriteAssets.TryGetValue(assetPath, out _loadedSpriteHandle))
{
if (_loadedSpriteHandle.IsValid() && _loadedSpriteHandle.Result != null)
{
BMLogger.Log($"AddressableManager: Loaded sprite from cache: {assetPath}");
return _loadedSpriteHandle.Result;
}
}
try
{
var handle = Addressables.LoadAssetAsync<Sprite>(assetPath.Trim());
await handle.Task;
if (handle.OperationException != null)
{
BMLogger.Log($"AddressableManager: Failed to load Sprite '{assetPath}': {handle.OperationException.Message}");
return null;
}
_loadedSpriteAssets[assetPath] = handle;
return handle.Result;
}
catch (Exception e)
{
BMLogger.LogError($"AddressableManager: Failed to load Sprite '{assetPath}': {e.Message}");
return null;
}
}
/// <summary>
/// True if this address is already in the audio cache (play immediately vs async load).
/// </summary>
public bool TryGetCachedAudioClip(string assetPath, out AudioClip clip)
{
clip = null;
if (string.IsNullOrEmpty(assetPath))
return false;
if (_loadedAudioAssets.TryGetValue(assetPath, out var h) && h.IsValid() && h.Result != null)
{
clip = h.Result;
return true;
}
return false;
}
/// <summary>
/// When the asset is no longer needed, call this method to unload it. <br/>
/// The asset will be released after a certain amount of time. <br/>
/// </summary>
/// <param name="assetPath">The asset path used when loading the asset</param>
public void ReleaseAsset(string assetPath)
{
// If multiple release requests are made for the same asset, we only need to store the earliest release time.
float currentReleaseTime = Time.realtimeSinceStartup;
if (_releaseAssetTimestamps.TryGetValue(assetPath, out var timeStamp))
{
currentReleaseTime = Mathf.Min(currentReleaseTime, timeStamp);
}
_releaseAssetTimestamps[assetPath] = currentReleaseTime;
}
public void ForceReleaseAsset(string assetPath)
{
if (_loadedPrefabAssets.TryGetValue(assetPath, out var loadedPrefabHandle))
{
if (loadedPrefabHandle.IsValid())
{
Addressables.Release(loadedPrefabHandle);
}
_loadedPrefabAssets.Remove(assetPath);
}
else if (_loadedTextAssets.TryGetValue(assetPath, out var loadedTextHandle))
{
if (loadedTextHandle.IsValid())
{
Addressables.Release(loadedTextHandle);
}
_loadedTextAssets.Remove(assetPath);
}
else if (_loadedAudioAssets.TryGetValue(assetPath, out var loadedAudioHandle))
{
if (loadedAudioHandle.IsValid())
{
Addressables.Release(loadedAudioHandle);
}
_loadedAudioAssets.Remove(assetPath);
BMLogger.Log($"AddressableManager: Force released audio asset: {assetPath}");
}
else if (_loadedSpriteAssets.TryGetValue(assetPath, out var loadedSpriteHandle))
{
if (loadedSpriteHandle.IsValid())
{
Addressables.Release(loadedSpriteHandle);
}
_loadedSpriteAssets.Remove(assetPath);
BMLogger.Log($"AddressableManager: Force released sprite asset: {assetPath}");
}
}
/// <summary>
/// Release a specific asset by its handle directly.
/// </summary>
/// <param name="handle">The async operation handle to release</param>
public void ReleaseAsset(AsyncOperationHandle<GameObject> handle)
{
if (handle.IsValid())
{
Addressables.Release(handle);
}
}
/// <summary>
/// Release all loaded assets from the cache.
/// </summary>
public void ReleaseAllAssets()
{
foreach (var kvp in _loadedPrefabAssets)
{
if (kvp.Value.IsValid())
{
Addressables.Release(kvp.Value);
}
}
_loadedPrefabAssets.Clear();
foreach (var kvp in _loadedAudioAssets)
{
if (kvp.Value.IsValid())
{
Addressables.Release(kvp.Value);
}
}
_loadedAudioAssets.Clear();
foreach (var kvp in _loadedSpriteAssets)
{
if (kvp.Value.IsValid())
{
Addressables.Release(kvp.Value);
}
}
_loadedSpriteAssets.Clear();
BMLogger.Log("AddressableManager: Released all assets");
}
/// <summary>
/// Check if an asset is currently loaded in the cache.
/// </summary>
/// <param name="assetPath">The asset path to check</param>
/// <returns>True if the asset is loaded</returns>
public bool IsAssetLoaded(string assetPath)
{
return _loadedPrefabAssets.ContainsKey(assetPath) && _loadedPrefabAssets[assetPath].IsValid();
}
#if UNITY_EDITOR
private HashSet<string> _invalidAssetPaths = new();
#endif
/// <summary>
/// Checks if a given Addressable key or path exists in the current catalogs.
/// </summary>
/// <param name="key">The Addressable key, path, or label to check.</param>
/// <param name="type">Optional: The specific type of asset you are looking for.</param>
/// <returns>True if the key exists, false otherwise.</returns>
public static bool KeyExists(object key, System.Type type = null)
{
string keyStr = key?.ToString();
if (string.IsNullOrEmpty(keyStr))
return false;
#if UNITY_EDITOR
// Failed loads are cached; still verify against catalog (do not assume unknown keys exist).
if (Instance != null && Instance._invalidAssetPaths.Contains(keyStr))
return false;
#endif
foreach (IResourceLocator locator in Addressables.ResourceLocators)
{
if (locator.Locate(key, type, out var locations)
&& locations != null
&& locations.Count > 0)
{
return true;
}
}
return false;
}
#endregion
private void OnDestroy()
{
OnDispose?.Invoke();
ReleaseAllAssets();
}
}
}