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 { /// /// Scene Bootstrap (index 1): Addressables luôn được init thẳng vì /// đã chạy xong ở scene 0 ( = true). /// [DefaultExecutionOrder(-1990)] public class AddressableManager : MonoSingleton { private bool _isInitialized = false; private UniTaskCompletionSource _initializationTcs; private Dictionary> _loadedPrefabAssets = new(); private Dictionary> _loadedTextAssets = new(); private Dictionary> _loadedAudioAssets = new(); private Dictionary> _loadedSpriteAssets = new(); private Dictionary> _loadedSpriteAtlasAssets = new(); private Dictionary _loadedAssetReferenceCount = new(); /// /// 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. /// private Dictionary _releaseAssetTimestamps = new(); [SerializeField]private float _releaseAssetTimeout = 10f; public event Action OnDispose; /// Get the count of currently loaded assets. public int LoadedPrefabCount => _loadedPrefabAssets.Count; public bool IsInitialized() => _isInitialized; /// /// Returns immediately if already initialized; otherwise waits for the /// Addressables initialization callback without per-frame polling. /// public UniTask WaitUntilInitializedAsync() { if (_isInitialized) return UniTask.CompletedTask; return _initializationTcs.Task; } protected override void Awake() { Debug.Log($"[Cuong] AddressableManager: Awake | id={GetInstanceID()} frame={Time.frameCount}"); base.Awake(); } protected override void Initialize() { base.Initialize(); _isInitialized = false; _initializationTcs = new UniTaskCompletionSource(); StartAddressablesInitAsync().Forget(); } async UniTaskVoid StartAddressablesInitAsync() { Debug.Log("[Cuong] AddressableManager: Đang InitializeAsync Addressables..."); try { await AddressablesInitService.EnsureInitializedAsync(); _isInitialized = true; _initializationTcs.TrySetResult(); BMLogger.Log("AddressableManager: Initialized"); Debug.Log("[Cuong] AddressableManager: InitializeAsync xong — sẵn sàng load asset."); } catch (Exception e) { BMLogger.LogError($"AddressableManager: Failed to initialize: {e.Message}"); Debug.LogError($"[Cuong] AddressableManager: InitializeAsync thất bại — {e.Message}"); } } #region Unity lifecycle private List _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 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 _loadedTextAssetHandle; /// /// Load a text asset asynchronously. /// NOTE: The key must match the Addressables "Address" (or another valid key like a label/GUID). /// /// /// /// public async Task 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(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 _loadedSpriteAtlasHandle; /// Load a sprite atlas asynchronously. public async Task 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(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 _loadedPrefabHandle; /// /// Load an asset asynchronously. The address should look like this: "models/npcs/npc/魅灵首领/魅灵首领/魅灵首领.prefab" /// /// public async Task 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(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 _loadedAudioClipHandle; /// /// Load an AudioClip asynchronously (e.g. skill SFX). Key must match the Addressables address. /// public async Task LoadAudioClipAsync(string 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(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 _loadedSpriteHandle; /// /// Load a Sprite asynchronously (e.g. UI art). Key must match the Addressables address. /// public async Task 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(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; } } /// /// True if this address is already in the audio cache (play immediately vs async load). /// 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; } /// /// When the asset is no longer needed, call this method to unload it.
/// The asset will be released after a certain amount of time.
///
/// The asset path used when loading the asset 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}"); } } /// /// Release a specific asset by its handle directly. /// /// The async operation handle to release public void ReleaseAsset(AsyncOperationHandle handle) { if (handle.IsValid()) { Addressables.Release(handle); } } /// /// Release all loaded assets from the cache. /// 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"); } /// /// Check if an asset is currently loaded in the cache. /// /// The asset path to check /// True if the asset is loaded public bool IsAssetLoaded(string assetPath) { return _loadedPrefabAssets.ContainsKey(assetPath) && _loadedPrefabAssets[assetPath].IsValid(); } #if UNITY_EDITOR private HashSet _invalidAssetPaths = new(); #endif /// /// Checks if a given Addressable key or path exists in the current catalogs. /// /// The Addressable key, path, or label to check. /// Optional: The specific type of asset you are looking for. /// True if the key exists, false otherwise. 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(); } } }