using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Threading.Tasks; using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.ResourceManagement.AsyncOperations; using UnityEngine.U2D; using static UnityEngine.AddressableAssets.Addressables; /// /// 战神预制体用途:战神有两个预制体(展示/游戏内使用),名字相同,靠 Addressables 子标签区分。 /// - Display:Default 子标签(大厅/商城预览模型) /// - GamePlay:ZSYX 子标签(座位上替代普通炮台的使用模型) /// public enum ZhanShenUsage { /// 展示用:大厅/商城预览模型(Default 子标签加载) Display = 0, /// 游戏内用:座位上替代普通炮台的模型(ZSYX 子标签加载) GamePlay = 1 } [Serializable] public class TurretResourcesData { public int turretNumber; public string turretName; // 中文名称(如"深海之眼") public int turretType; // 1=普通炮台 2=战神 3=翅膀 public string prefabTurretResourcesPath; // 资源路径(prefab/spine/sprite 共用名) public int animationType; // 1=spine 2=animator 3=animation public int prefabType; // 预制体类型(战神分类:1=国色天香 2=西游传说 3=山海奇闻;炮台/翅膀=0) public int attackAudioID; // 攻击音效 ID public List bulletID; // 子弹 ID 列表 // ---- Addressables 标签 ---- const string turret_AALabel = "PTCBZS"; // 炮台统一标签(主标签:炮台/翅膀/战神共用) const string defaultSubLabel = "Default"; // 展示子标签(展示用预制体:普通炮台/翅膀/战神展示) const string zhanShenGameSubLabel = "ZSYX"; // 战神游戏内子标签(战神的使用预制体,与展示同名靠标签区分) // ---- 路径→ID 映射(用于 Addressables 加载时反查) ---- private static Dictionary turretPrefabPathDic; // prefabPath → turretNumber private static Dictionary turretSpritePathDic; // spritePath → turretNumber // ---- ID→数据/资源 映射 ---- public static Dictionary turretDataDic; // id → data private static Dictionary turretPrefabDic; // id → 展示用预制体(Default 子标签) private static Dictionary turretGamePrefabDic; // id → 战神游戏内使用预制体(ZSYX 子标签) private static Dictionary turretSpriteDic; // id → sprite // ---- 保留的 AA Handle(不释放,避免 Build 下资源被卸载)---- private static readonly List _loadedHandles = new List(); // ==================== 战神按需加载(散包:用哪个加载哪个) ==================== // 每个战神单独打标签 ZS{编号}(如 ZS14901),展示 prefab = ZS{id}+Default,游戏内 = ZS{id}+ZSYX。 // 战神 prefab 不再带 PTCBZS 标签 → 启动全量加载(PT+Default/PT+ZSYX)不再拉出战神, // turretchibangzhanshenres 包只剩炮台/翅膀/sprite,战神包按需单独加载。 /// 战神 prefab 缓存:(编号, 用途) → prefab private static readonly Dictionary<(int id, ZhanShenUsage usage), GameObject> _zsPrefabCache = new Dictionary<(int, ZhanShenUsage), GameObject>(); /// 战神加载句柄:(编号, 用途) → handle(卸载用;LoadAssetsAsync 返回 IList 句柄) private static readonly Dictionary<(int id, ZhanShenUsage usage), AsyncOperationHandle>> _zsHandles = new Dictionary<(int, ZhanShenUsage), AsyncOperationHandle>>(); /// 战神引用计数:(编号, 用途) → 使用中的座位/预览数(归零才真正卸载,防止多座位共用误卸) private static readonly Dictionary<(int id, ZhanShenUsage usage), int> _zsRefCount = new Dictionary<(int, ZhanShenUsage), int>(); /// /// ⭐ 按需获取单个战神 prefab(异步 + 引用计数)。 /// 用途:进房/换战神(GamePlay)、大厅预览(Display)。 /// 每次调用引用 +1;切换/关闭时配对调用 ReleaseZhanShen(-1), /// 引用归零才真正卸载战神包 —— 多个座位用同一个战神不会互相误卸。 /// 标签:ZS{编号} + Default(展示)/ZSYX(游戏内) 交集;标签未配置时返回 null /// (调用方可回退 GetZhanShenPrefab 查全量字典,平滑迁移)。 /// public static async Task AcquireZhanShenAsync(int turretNumber, ZhanShenUsage usage) { var key = (turretNumber, usage); _zsRefCount[key] = _zsRefCount.TryGetValue(key, out int c) ? c + 1 : 1; if (_zsPrefabCache.TryGetValue(key, out var cached)) return cached; var keys = new List { "ZS" + turretNumber, usage == ZhanShenUsage.GamePlay ? zhanShenGameSubLabel : defaultSubLabel }; var handle = Addressables.LoadAssetsAsync(keys, null, MergeMode.Intersection); await handle.Task; if (handle.Status == AsyncOperationStatus.Succeeded && handle.Result != null && handle.Result.Count > 0) { // 展示/游戏内同名靠标签区分,交集结果取第一个 _zsPrefabCache[key] = handle.Result[0]; _zsHandles[key] = handle; return handle.Result[0]; } // 加载失败(标签未配置等):归还引用计数,不缓存句柄 if (handle.IsValid()) Addressables.Release(handle); _zsRefCount[key] = c; // 还原为进入时的值 if (_zsRefCount[key] <= 0) _zsRefCount.Remove(key); return null; } /// /// ⭐ 按需获取炮台/翅膀 prefab(异步 + 引用计数,标签 PT{编号},如 PT11001)。 /// 散包模式下启动不再全量加载 prefab,座位装炮台/大厅预览时按需取。 /// 与 ReleaseTurretPrefab 配对(切换/关闭时 -1,归零卸包)。 /// public static async Task AcquireTurretPrefabAsync(int turretNumber) { _ptRefCount[turretNumber] = _ptRefCount.TryGetValue(turretNumber, out int c) ? c + 1 : 1; if (_ptPrefabCache.TryGetValue(turretNumber, out var cached)) return cached; var handle = Addressables.LoadAssetsAsync(new List { "PT" + turretNumber }, null, MergeMode.Intersection); await handle.Task; if (handle.Status == AsyncOperationStatus.Succeeded && handle.Result != null && handle.Result.Count > 0) { _ptPrefabCache[turretNumber] = handle.Result[0]; _ptHandles[turretNumber] = handle; return handle.Result[0]; } if (handle.IsValid()) Addressables.Release(handle); _ptRefCount[turretNumber] = c; if (_ptRefCount[turretNumber] <= 0) _ptRefCount.Remove(turretNumber); return null; } /// 释放炮台/翅膀 prefab 引用(与 AcquireTurretPrefabAsync 配对,归零卸包) public static void ReleaseTurretPrefab(int turretNumber) { if (!_ptRefCount.TryGetValue(turretNumber, out int c) || c <= 0) return; if (c > 1) { _ptRefCount[turretNumber] = c - 1; return; } _ptRefCount.Remove(turretNumber); if (_ptHandles.TryGetValue(turretNumber, out var h) && h.IsValid()) Addressables.Release(h); _ptHandles.Remove(turretNumber); _ptPrefabCache.Remove(turretNumber); } /// 按需缓存:炮台/翅膀 prefab(PT{编号} 标签) private static readonly Dictionary _ptPrefabCache = new Dictionary(); private static readonly Dictionary>> _ptHandles = new Dictionary>>(); private static readonly Dictionary _ptRefCount = new Dictionary(); /// /// 强制卸载按需加载的炮台/翅膀 prefab(离房时调用,忽略引用计数)。 /// public static void UnloadAllTurretPrefabs() { var keys = new List(_ptHandles.Keys); foreach (var k in keys) { if (_ptHandles[k].IsValid()) Addressables.Release(_ptHandles[k]); } _ptHandles.Clear(); _ptPrefabCache.Clear(); _ptRefCount.Clear(); if (keys.Count > 0) Debug.Log($"[TurretResourcesData] 卸载炮台/翅膀按需 prefab {keys.Count} 项"); } /// /// 释放战神引用(切换战神/关闭预览时调用,与 AcquireZhanShenAsync 配对)。 /// 引用归零才真正卸载包;标签未配置走全量回退时此调用无害(没有计数项直接跳过)。 /// public static void ReleaseZhanShen(int turretNumber, ZhanShenUsage usage) { var key = (turretNumber, usage); if (!_zsRefCount.TryGetValue(key, out int c) || c <= 0) return; if (c > 1) { _zsRefCount[key] = c - 1; return; } // 归零:真正卸载 _zsRefCount.Remove(key); if (_zsHandles.TryGetValue(key, out var h) && h.IsValid()) Addressables.Release(h); _zsHandles.Remove(key); _zsPrefabCache.Remove(key); } /// /// 强制卸载按需加载的战神资源(离房时清游戏内的;面板关闭可清展示的)。 /// usage 传 null 清全部。忽略引用计数(离房场景所有引用者都已销毁)。 /// public static void UnloadAllZhanShen(ZhanShenUsage? usage = null) { var toRemove = new List<(int, ZhanShenUsage)>(); foreach (var kv in _zsHandles) { if (usage == null || kv.Key.usage == usage.Value) toRemove.Add(kv.Key); } foreach (var k in toRemove) { if (_zsHandles[k].IsValid()) Addressables.Release(_zsHandles[k]); _zsHandles.Remove(k); _zsPrefabCache.Remove(k); _zsRefCount.Remove(k); } if (toRemove.Count > 0) Debug.Log($"[TurretResourcesData] 卸载战神按需资源 {toRemove.Count} 项(usage={usage?.ToString() ?? "全部"})"); } static TurretResourcesData() { Init(); } /// /// 从 Resources/tbturretresources.json 加载炮台元数据。 /// 表里没有的 turretNumber,调用方用 TurretData 自身的 Inspector 默认值,不报错。 /// async static void Init() { turretPrefabPathDic = new Dictionary(); turretSpritePathDic = new Dictionary(); turretDataDic = new Dictionary(); TextAsset jsonAsset = Resources.Load("tbturretresources"); if (jsonAsset == null) { Debug.LogWarning("[TurretResourcesData] 资源表 tbturretresources.json 未找到,炮台数据字典为空,将使用 TurretData 默认值"); return; } try { var list = JsonConvert.DeserializeObject>(jsonAsset.text); if (list == null) { Debug.LogWarning("[TurretResourcesData] 资源表解析为空,将使用 TurretData 默认值"); return; } foreach (var item in list) { // 战神(turretType==2)的 prefabTurretResourcesPath 可能为空,不跳过 // 用 turretNumber 构造默认路径,以便 Addressables 按名匹配 if (string.IsNullOrEmpty(item.prefabTurretResourcesPath)) { if (item.turretType == 2) item.prefabTurretResourcesPath = item.turretNumber.ToString(); else continue; // 炮台/翅膀路径为空则跳过 } turretPrefabPathDic[item.prefabTurretResourcesPath] = item.turretNumber; turretSpritePathDic[item.prefabTurretResourcesPath] = item.turretNumber; turretDataDic[item.turretNumber] = item; Debug.Log($"[TurretResourcesData] 加载炮台: ID={item.turretNumber}, Path={item.prefabTurretResourcesPath}, Type={item.turretType}, PrefabType={item.prefabType}, BulletIDs=[{string.Join(",", item.bulletID)}]"); } Debug.Log($"[TurretResourcesData] 初始化完成,共 {turretDataDic.Count} 个炮台配置"); } catch (System.Exception ex) { Debug.LogError($"[TurretResourcesData] JSON 解析失败: {ex.Message},炮台数据字典为空,将使用 TurretData 默认值"); } } // ==================== 资源加载 ==================== /// /// 加载炮台相关 Addressables 资源(两组标签交集): /// - PT + Default:普通炮台/翅膀/战神展示预制体 → turretPrefabDic; /// - PT + ZSYX:战神游戏内使用预制体 → turretGamePrefabDic(与展示同名,靠标签区分)。 /// 注意:Handle 必须保留,不能 Release,否则 Build 下资源会被立即卸载, /// 字典中存的就是已销毁对象的引用(Editor 下因 AssetDatabase 模式不会暴露此问题)。 /// public static async Task LoadTurretAssetsData() { turretPrefabDic = new Dictionary(); turretGamePrefabDic = new Dictionary(); turretSpriteDic = new Dictionary(); // 清理上一轮残留的 Handle foreach (var h in _loadedHandles) { if (h.IsValid()) Addressables.Release(h); } _loadedHandles.Clear(); // 展示用标签组:PT + Default(普通炮台/翅膀/战神展示预制体) var labels = new List { turret_AALabel, defaultSubLabel }; // 战神游戏内标签组:PT + ZSYX(战神的使用预制体,名字与展示相同) var zsGameLabels = new List { turret_AALabel, zhanShenGameSubLabel }; Debug.Log("[TurretResourcesData] 开始预下载炮台资源依赖 (PT + Default)..."); var depHandle = Addressables.DownloadDependenciesAsync(labels, MergeMode.Intersection); await depHandle.Task; Addressables.Release(depHandle); // 预下载 Handle 可以立即释放,只影响下载缓存 Debug.Log("[TurretResourcesData] 炮台依赖资源预下载完成!"); Debug.Log("[TurretResourcesData] 开始预下载战神游戏内资源依赖 (PT + ZSYX)..."); var zsDepHandle = Addressables.DownloadDependenciesAsync(zsGameLabels, MergeMode.Intersection); await zsDepHandle.Task; Addressables.Release(zsDepHandle); Debug.Log("[TurretResourcesData] 战神游戏内依赖资源预下载完成!"); // ⭐ 散包模式:炮台/翅膀/战神的 prefab 不再启动全量加载(每个资源一个小包, // 全量会把上百个散包全部拉起 —— 刚登录 194 个包的根源)。 // prefab 全部按需:炮台/翅膀走 AcquireTurretPrefabAsync(PT{编号} 标签), // 战神走 AcquireZhanShenAsync(ZS{编号}+Default/ZSYX 标签)。 // 启动只加载列表小图 Sprite(若 Sprite 与 prefab 同包仍会拉起散包, // 应把列表小图合入图集 —— 见 SpriteAtlasBuilderWindow 工具)。 // ---- 加载炮台 Sprite(列表图标,全量保留)---- var spriteHandle = Addressables.LoadAssetsAsync(labels, data => { if (turretSpritePathDic.TryGetValue(data.name, out int id)) { turretSpriteDic[id] = data; } }, MergeMode.Intersection); await spriteHandle.Task; // 不释放!保留 Handle 以保持资源存活 _loadedHandles.Add(spriteHandle); // ⭐ 图集兜底:列表小图已合入 SpriteAtlas 且 sprite 从组里移除(标签查不到)时, // 按图集标签 "TurretAtlas" 加载图集,用 GetSprite(名) 填表。 // (图集方式下 sprite 仍在图集里保留原名 = prefabTurretResourcesPath) if (turretSpriteDic.Count == 0) { try { var atlasHandle = Addressables.LoadAssetsAsync("TurretAtlas", null); await atlasHandle.Task; if (atlasHandle.Status == AsyncOperationStatus.Succeeded && atlasHandle.Result != null && atlasHandle.Result.Count > 0) { var atlas = atlasHandle.Result[0]; foreach (var kv in turretSpritePathDic) { var sp = atlas.GetSprite(kv.Key); if (sp != null) turretSpriteDic[kv.Value] = sp; } _loadedHandles.Add(atlasHandle); Debug.Log($"[TurretResourcesData] 图集方式加载 Sprite 完成,共 {turretSpriteDic.Count} 个(来自 {atlas.name})"); } } catch (Exception e) { Debug.LogWarning($"[TurretResourcesData] 图集兜底加载失败(未找到 TurretAtlas 标签的图集?): {e.Message}"); } } else { Debug.Log($"[TurretResourcesData] 炮台 Sprite 加载完成,共 {turretSpriteDic.Count} 个(prefab 已改按需加载)"); } } /// /// 卸载炮台 Addressables 资源。 /// 释放 LoadTurretAssetsData 中保留的所有 Handle。 /// public static Task UnLoadTurretAssetsData() { foreach (var h in _loadedHandles) { if (h.IsValid()) { Addressables.Release(h); Debug.Log($"[TurretResourcesData] 释放 Handle: {h.DebugName}"); } } _loadedHandles.Clear(); turretPrefabDic?.Clear(); turretGamePrefabDic?.Clear(); turretSpriteDic?.Clear(); Debug.Log($"[TurretResourcesData] 卸载完成,剩余 Prefab: {turretPrefabDic?.Count ?? 0}, 战神游戏内: {turretGamePrefabDic?.Count ?? 0}, Sprite: {turretSpriteDic?.Count ?? 0}"); return Task.CompletedTask; } // ==================== 查询接口 ==================== /// 根据 ID 获取炮台数据。 public static TurretResourcesData GetTurretData(int turretNumber) { return turretDataDic != null && turretDataDic.TryGetValue(turretNumber, out var data) ? data : null; } /// 根据 ID 获取炮台 Prefab。⭐ 优先查按需缓存(PT{编号} 标签),回退全量字典(已不再填充,恒空)。 public static GameObject GetTurretPrefab(int turretNumber) { if (_ptPrefabCache.TryGetValue(turretNumber, out var prefab)) return prefab; return turretPrefabDic != null && turretPrefabDic.TryGetValue(turretNumber, out var fullLoad) ? fullLoad : null; } /// /// 根据战神编号获取战神预制体(两种情况二选一)。 /// - Display:展示用(大厅/商城预览,与 GetTurretPrefab 返回同一个展示模型); /// - GamePlay:游戏内用(座位上替代普通炮台的使用模型)。 /// ⭐ 优先查按需加载缓存(散包);未加载时回退全量加载的字典(标签未拆时的平滑迁移)。 /// public static GameObject GetZhanShenPrefab(int turretNumber, ZhanShenUsage usage = ZhanShenUsage.Display) { // 按需缓存优先 if (_zsPrefabCache.TryGetValue((turretNumber, usage), out var prefab)) return prefab; // 回退:全量加载的字典(战神 prefab 未去 PTCBZS 标签时仍会被启动全量加载进来) var dic = usage == ZhanShenUsage.GamePlay ? turretGamePrefabDic : turretPrefabDic; return dic != null && dic.TryGetValue(turretNumber, out var fullLoad) ? fullLoad : null; } /// 根据 ID 获取炮台 Sprite。 public static Sprite GetTurretSprite(int turretNumber) { return turretSpriteDic != null && turretSpriteDic.TryGetValue(turretNumber, out var sprite) ? sprite : null; } /// 获取所有炮台数据列表。 public static List GetAllTurretData() { return turretDataDic != null ? new List(turretDataDic.Values) : new List(); } /// 根据类型获取炮台数据(1=普通炮台, 2=战神, 3=翅膀)。 public static List GetTurretDataByType(int turretType) { var result = new List(); if (turretDataDic == null) return result; foreach (var kv in turretDataDic) { if (kv.Value.turretType == turretType) result.Add(kv.Value); } return result; } }