using Newtonsoft.Json; using Spine.Unity; using System; using System.Collections; using System.Collections.Generic; using System.Threading.Tasks; using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.ResourceManagement.AsyncOperations; using static UnityEngine.AddressableAssets.Addressables; [Serializable] public class FishResourcesData { public int fishNumber; public string prefabFishResourcesPath; public string dieEffectAssets; /// /// 出场音频 /// public List showAudio; /// /// 移动音频 /// public List moveAudio; /// /// 死亡音效 1,2,3 /// public List dieAudio; /// /// 爆奖音效 /// public Dictionary dieGlodAduio; const string prefabFish_AALabel="FishPrefab"; const string dieSpine_AALabel = "DieSpine"; const string Sprite_AALabel = "TuJian"; private static Dictionary FishResourcesPrefabsDic ;//prefabpath--id private static Dictionary FishResourcesSpineDieDic ;//dieSpinepath--id private static Dictionary FishResourcesSpriteDic;//Spritepath--id static FishResourcesData() { Init(); } async static void Init() { string prefabFishPath = Resources.Load("tbfishresources").text; var v = JsonConvert.DeserializeObject>(prefabFishPath); FishResourcesPrefabsDic = new Dictionary(); fishResourcesDatasDic = new Dictionary(); FishResourcesSpineDieDic = new Dictionary(); FishResourcesSpriteDic = new Dictionary(); foreach (var item in v) { if (item.prefabFishResourcesPath != null && item.prefabFishResourcesPath != "") { Debug.Log($"添加鱼资源数据: {item.fishNumber} - {item.prefabFishResourcesPath}"); FishResourcesPrefabsDic.Add(item.prefabFishResourcesPath, item.fishNumber); FishResourcesSpriteDic.Add(item.prefabFishResourcesPath, item.fishNumber); if (item.dieEffectAssets != null && item.dieEffectAssets != "") { FishResourcesSpineDieDic.Add(item.dieEffectAssets, item.fishNumber); } fishResourcesDatasDic.Add(item.fishNumber, item); } } } private static Dictionary fishPrefabDic;//id--prefabs public static Dictionary fishResourcesDatasDic;//id--FishResourcesData public static Dictionary fishResourcesSpritesDic;//id--Sprite private static Dictionary diePrefabDic;//id--SkeletonDataAsset // ⭐ 句柄显式管理(修复"包只增不减"的根源): // 旧实现加载完立即 Release 句柄,但 fishPrefabDic 还持有 prefab 引用 —— // 包没被卸载全靠 AudioResourcesData 泄漏的音频句柄"误打误撞"撑着(音频物理上就在鱼包里)。 // 现在:加载时保存句柄(prefab/dieSpine/sprite 三类),离房时 UnLoadFishAssetsData 统一 Release, // 引用归零后鱼包真正从内存卸载 —— 依次进所有玩法不再累加内存。 private static readonly List _fishHandles = new List(); /// /// 加载当前玩法的鱼资源(prefab / 死亡Spine / 图鉴Sprite)。 /// onProgress:整体进度回调 0~1(prefab 0~0.62 / spine 0.62~0.81 / sprite 0.81~1,按各段实际 PercentComplete 细分), /// 供进房进度条使用;不传则行为与原来一致。 /// public static async Task LoadFishAssetsData(Action onProgress = null) { // ⭐ 资源加载超过 0.1 秒才显示"加载中"界面(引用计数,快速加载不显示) UIManager.BeginLoading(); try { await LoadFishAssetsDataCore(onProgress); } finally { UIManager.EndLoading(); } } /// LoadFishAssetsData 的实际逻辑(外层负责"加载中"界面) private static async Task LoadFishAssetsDataCore(Action onProgress) { // 清空对象池 Pool.Instance.Clear(); fishPrefabDic = new Dictionary(); diePrefabDic = new Dictionary(); fishResourcesSpritesDic = new Dictionary(); // ⭐ 防御:上一轮句柄未释放(如离房流程被跳过)时先释放,避免引用计数累加 foreach (var h in _fishHandles) { if (h.IsValid()) Addressables.Release(h); } _fishHandles.Clear(); // ---------------- 1. 预下载所有依赖资源(关键) ---------------- var labels = new List { prefabFish_AALabel, dieSpine_AALabel, Sprite_AALabel, GameManager.Instance.gameData.currGameType.ToString() }; Debug.Log("开始预下载所有鱼资源依赖..."); var depHandle = Addressables.DownloadDependenciesAsync(labels, MergeMode.Intersection); await depHandle.Task; Addressables.Release(depHandle); Debug.Log("依赖资源预下载完成!"); // ---------------- 2. 加载 Prefab(0 ~ 0.62,最重的一段) ---------------- var prefabKeys = new List { prefabFish_AALabel, GameManager.Instance.gameData.currGameType.ToString() }; var prefabHandle = Addressables.LoadAssetsAsync(prefabKeys, prefab => { if (FishResourcesPrefabsDic.TryGetValue(prefab.name, out int id)) { fishPrefabDic[id] = prefab; var mc = prefab.GetComponent(); if (mc != null) mc.fishTypeID = id; // ⭐ 统一用 Pool.Instance 的 poolObj 作为父节点(不再用 CreateFishManager.poolRoot), // 这样 Pool.Clear() 销毁 poolObj 时能连带销毁所有 poolList_XXX 子节点,避免残留 Pool.Instance.dic[id] = new PoolData(prefab, Pool.Instance.EnsurePoolRoot(), id); } }, MergeMode.Intersection); await AwaitWithProgress(prefabHandle, 0f, 0.62f, onProgress); // ⭐ 不再立即 Release:保存句柄保持包存活,离房时统一卸载 _fishHandles.Add(prefabHandle); Debug.Log($"Prefab 加载完成,共 {fishPrefabDic.Count} 个"); // ---------------- 3. 加载死亡 Spine 动效(0.62 ~ 0.81) ---------------- // 注意:某些游戏类型下 Addressables 中 "DieSpine"+"游戏类型" 两个 label 可能没有交集资源, // 会抛 InvalidKeyException。这里用 try-catch 容错,避免单个资源类型加载失败阻塞整体流程。 try { var dieKeys = new List { dieSpine_AALabel, GameManager.Instance.gameData.currGameType.ToString() }; var dieHandle = Addressables.LoadAssetsAsync(dieKeys, data => { if (FishResourcesSpineDieDic.TryGetValue(data.name, out int id)) { diePrefabDic[id] = data; } }, MergeMode.Intersection); await AwaitWithProgress(dieHandle, 0.62f, 0.81f, onProgress); _fishHandles.Add(dieHandle); Debug.Log($"死亡 Spine 加载完成,共 {diePrefabDic.Count} 个"); } catch (Exception e) { onProgress?.Invoke(0.81f); Debug.LogWarning($"[FishResourcesData] 死亡 Spine 加载失败(游戏类型={GameManager.Instance.gameData.currGameType}),跳过此项: {e.Message}"); } // ---------------- 4. 加载鱼 Sprite(0.81 ~ 1) ---------------- try { var spriteKeys = new List { Sprite_AALabel, GameManager.Instance.gameData.currGameType.ToString() }; var spriteHandle = Addressables.LoadAssetsAsync(spriteKeys, data => { if (FishResourcesSpriteDic.TryGetValue(data.name, out int id)) { fishResourcesSpritesDic[id] = data; } }, MergeMode.Intersection); await AwaitWithProgress(spriteHandle, 0.81f, 1f, onProgress); _fishHandles.Add(spriteHandle); Debug.Log($"Sprite 加载完成,共 {fishResourcesSpritesDic.Count} 个"); } catch (Exception e) { onProgress?.Invoke(1f); Debug.LogWarning($"[FishResourcesData] Sprite 加载失败(游戏类型={GameManager.Instance.gameData.currGameType}),跳过此项: {e.Message}"); } onProgress?.Invoke(1f); Debug.Log("所有鱼资源加载完毕!"); } /// /// 等待句柄完成并按其 PercentComplete 映射到 [from, to] 区间上报进度。 /// 50ms 轮询(进度条 20fps 刷新足够),完成后兜底上报 to。 /// private static async Task AwaitWithProgress(AsyncOperationHandle handle, float from, float to, Action onProgress) { if (onProgress == null) { await handle.Task; return; } while (!handle.IsDone) { onProgress(Mathf.Lerp(from, to, handle.PercentComplete)); await Task.Delay(50); } onProgress(to); } public static Task UnLoadFishAssetsData() { // ⭐ 真正的卸载:释放加载时保存的全部句柄(引用归零后鱼包才会真正卸载)。 // 旧实现"重新 Load 一遍拿句柄再 Release"是净 0 操作,什么都卸不掉。 Pool.Instance.Clear(); foreach (var h in _fishHandles) { if (h.IsValid()) Addressables.Release(h); } _fishHandles.Clear(); // ⭐ 清空字典(包卸载后对象已失效,残留引用会指向已销毁资源) fishPrefabDic?.Clear(); diePrefabDic?.Clear(); fishResourcesSpritesDic?.Clear(); Debug.Log($"[FishResourcesData] 鱼资源卸载完成"); return Task.CompletedTask; } public static GameObject GetFishPrefab(int fishNumber) { if (fishPrefabDic != null && fishPrefabDic.ContainsKey(fishNumber)) { return fishPrefabDic[fishNumber]; } else { Debug.LogError($"鱼Prefab未找到: {fishNumber}"); return null; } } public static SkeletonDataAsset GetFishDieSpine(int fishNumber) { if (diePrefabDic != null && diePrefabDic.ContainsKey(fishNumber)) { return diePrefabDic[fishNumber]; } else { Debug.LogError($"鱼死亡动效未找到: {fishNumber}"); return null; } } }