527 lines
20 KiB
C#
527 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)
|
|
{
|
|
#if UNITY_EDITOR
|
|
return !Instance._invalidAssetPaths.Contains(key.ToString());
|
|
#else
|
|
// Iterate through all loaded locators (catalogs)
|
|
foreach (IResourceLocator locator in Addressables.ResourceLocators)
|
|
{
|
|
// If the locator finds the key, it returns true
|
|
if (locator.Locate(key, type, out var locations))
|
|
{
|
|
// Double check that it actually yielded at least one location
|
|
if (locations != null && locations.Count > 0)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
#endif
|
|
}
|
|
#endregion
|
|
|
|
|
|
|
|
private void OnDestroy()
|
|
{
|
|
OnDispose?.Invoke();
|
|
ReleaseAllAssets();
|
|
}
|
|
}
|
|
}
|