using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.AddressableAssets.ResourceLocators; using UnityEngine.ResourceManagement.AsyncOperations; namespace BrewMonster.Scripts { public class AddressableManager : MonoSingleton { private bool _isInitialized = false; private Dictionary> _loadedPrefabAssets = new(); private Dictionary> _loadedTextAssets = 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; protected override void Initialize() { base.Initialize(); _isInitialized = false; Addressables.InitializeAsync().Completed += OnInitializeComplete; } #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 OnInitializeComplete(AsyncOperationHandle handle) { if (handle.Status == AsyncOperationStatus.Succeeded) { _isInitialized = true; 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); } } #endregion #region public functions /// /// 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) { // remove the asset from the release timestamp dictionary. So it won't be released. RemoveFromReleaseAssetDictionary(assetPath); if (_loadedTextAssets.ContainsKey(assetPath)) { return _loadedTextAssets[assetPath].Result; } 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; } } /// /// 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 (_loadedPrefabAssets.ContainsKey(assetPath)) { BMLogger.Log($"AddressableManager: Asset already loaded: {assetPath} is valid: (${_loadedPrefabAssets[assetPath].Result != null})"); return _loadedPrefabAssets[assetPath].Result; } try { var handle = Addressables.LoadAssetAsync(assetPath); await handle.Task; _loadedPrefabAssets[assetPath] = handle; return handle.Result; } catch (System.Exception e) { BMLogger.LogError(e.StackTrace); return null; } } /// /// 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); BMLogger.Log($"AddressableManager: Released asset: {assetPath}"); } else if (_loadedTextAssets.TryGetValue(assetPath, out var loadedTextHandle)) { if (loadedTextHandle.IsValid()) { Addressables.Release(loadedTextHandle); } _loadedTextAssets.Remove(assetPath); BMLogger.Log($"AddressableManager: Released text 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(); 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(); } #endregion private void OnDestroy() { OnDispose?.Invoke(); ReleaseAllAssets(); } } }