hch
2025-12-03 c2c98d54516b45118bd4061317b3e291539119c5
Merge branch 'master' of http://mobile.secondworld.net.cn:10010/r/Project_SG_scripts
5个文件已修改
19个文件已添加
1576 ■■■■■ 已修改文件
Main/Component/UI/Effect/BattleEffectPlayer.cs 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/BattleField/BattleField.cs 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/BattleField/RecordActions/PreloadResAction.cs 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/BattleField/RecordActions/PreloadResAction.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/BattleObject/BattleObjectFactory.cs 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/BattleResources.meta 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/BattleResources/BattleAudioResLoader.cs 125 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/BattleResources/BattleAudioResLoader.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/BattleResources/BattleCacheManager.cs 462 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/BattleResources/BattleCacheManager.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/BattleResources/BattlePreloadManager.cs 168 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/BattleResources/BattlePreloadManager.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/BattleResources/BattleResCache.cs 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/BattleResources/BattleResCache.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/BattleResources/BattleResManager.cs 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/BattleResources/BattleResManager.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/BattleResources/BattleSpineResLoader.cs 125 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/BattleResources/BattleSpineResLoader.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/BattleResources/BattleUnloadManager.cs 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/BattleResources/BattleUnloadManager.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/BattleResources/TeamResTracker.cs 192 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/BattleResources/TeamResTracker.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/RecordPlayer/RecordActionType.cs 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/Sound/BattleSoundManager.cs 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Component/UI/Effect/BattleEffectPlayer.cs
@@ -353,15 +353,27 @@
    protected void PlaySpineEffect()
    {
        //  这里是纯spine的逻辑
        if (spineComp == null)
        {
            Debug.LogError("BattleEffectPlayer spineComp is null, effect id is " + effectId);
            return;
        }
        SkeletonDataAsset skeletonDataAsset = ResManager.Instance.LoadAsset<SkeletonDataAsset>("UIEffect/" + effectConfig.packageName, effectConfig.fxName);
        // ===== 简化:直接从缓存获取,缓存内部会自动加载 =====
        string directory = "UIEffect/" + effectConfig.packageName;
        SkeletonDataAsset skeletonDataAsset = BattleResManager.Instance.GetSpineResource(
            directory,
            effectConfig.fxName,
            battleField?.guid
        );
        if (skeletonDataAsset == null)
        {
            Debug.LogError($"BattleEffectPlayer: Failed to load effect spine {effectConfig.fxName}");
            return;
        }
        // ================================
        spineComp.skeletonDataAsset = skeletonDataAsset;
        spineComp.Initialize(true);
        spineComp.timeScale = speedRate;
Main/System/Battle/BattleField/BattleField.cs
@@ -145,6 +145,8 @@
        rejectNewPackage = false;
        OnRoundChange?.Invoke(round, turnMax);
        PreloadResources(redTeamList, blueTeamList);
#if UNITY_EDITOR
        if (Launch.Instance.isOpenSkillLogFile)
        {
@@ -182,7 +184,19 @@
        }
#endif
    }
    private void PreloadResources(List<TeamBase> redTeamList, List<TeamBase> blueTeamList)
    {
        if (blueTeamList == null || blueTeamList.Count <= 0)
        {
            return;
        }
        // 传递战场GUID
        PreloadResAction preloadAction = new PreloadResAction(this, redTeamList, blueTeamList);
        recordPlayer.PlayRecord(preloadAction);
    }
    protected virtual void LoadMap(int mapID)
    {
        BattleMapConfig battleMapConfig = BattleMapConfig.Get(mapID);
@@ -244,6 +258,9 @@
    public virtual void Run()
    {
        //  清理音频
        soundManager.Run();
        if (IsPause)
            return;
@@ -605,6 +622,9 @@
            // 战场自身的结束逻辑,不含结算等外部逻辑
            OnSettlement(turnFightStateData);
            BattleResManager.Instance.UnloadBattleResources(guid);
            int winFaction = (int)turnFightStateData["winFaction"];
            //获胜阵营:   一般为1或者2,当玩家发起的战斗时,如果获胜阵营不等于1代表玩家失败了
@@ -641,6 +661,9 @@
        
        // 清理死亡处理记录
        processingDeathObjIds.Clear();
        // ===== 新增:卸载蓝队资源 =====
        BattleResManager.Instance.UnloadBattleResources(guid);
    }
    //清场敌方但不终止战斗,用于切换主线BOSS战斗后,正常显示敌方
Main/System/Battle/BattleField/RecordActions/PreloadResAction.cs
New file
@@ -0,0 +1,61 @@
using UnityEngine;
using System.Collections.Generic;
public class PreloadResAction : RecordAction
{
    private List<TeamBase> redTeamList;
    private List<TeamBase> blueTeamList;
    public PreloadResAction(BattleField _battleField, List<TeamBase> _redTeamList, List<TeamBase> _blueTeamList)
        : base(RecordActionType.PreloadRes, _battleField, null)
    {
        redTeamList = _redTeamList;
        blueTeamList = _blueTeamList;
    }
    public override bool IsFinished()
    {
        return isFinish;
    }
    public override void Run()
    {
        base.Run();
        if (isRunOnce)
        {
            return;
        }
        // 传递战场GUID
        BattleResManager.Instance.PreloadBattleResources(
            battleField.guid,  // ← 关键:传递战场GUID
            redTeamList,
            blueTeamList,
            (progress) =>
            {
                BattleDebug.LogError($"Battle {battleField.guid} resources loading: {progress * 100}%");
            },
            OnPreloadFinish
        );
        isRunOnce = true;
    }
    private void OnPreloadFinish()
    {
        BattleDebug.LogError("Battle resources preload complete.");
        isFinish = true;
    }
    public override void ForceFinish()
    {
        //正常开始之后到界面出现之前都点不了 所以这边不用强制完成 接口留着
        base.ForceFinish();
        // 完成就开始显示UI
    }
}
Main/System/Battle/BattleField/RecordActions/PreloadResAction.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e694db14c5e25c6488fb04c8847759ae
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/System/Battle/BattleObject/BattleObjectFactory.cs
@@ -24,12 +24,26 @@
    //  这里报错了检查一下
    public static BattleObject CreateBattleObject(BattleField _battleField, List<GameObject> posNodeList, TeamHero teamHero, BattleCamp _Camp)
    {
        HeroSkinConfig skinCfg = teamHero.skinConfig;
        var skinCfg = HeroSkinConfig.Get(teamHero.SkinID);
        if (skinCfg == null)
        {
            Debug.LogError(teamHero.heroId + "BattleObjectFactory.CreateBattleObject: skinCfg is null for " + teamHero.SkinID);
            Debug.LogError($"BattleObjectFactory: skinCfg is null for SkinID {teamHero.SkinID}");
            return null;
        }
        // ===== 简化:直接从缓存获取,缓存内部会自动加载 =====
        SkeletonDataAsset skeletonDataAsset = BattleResManager.Instance.GetSpineResource(
            "Hero/SpineRes/",
            skinCfg.SpineRes,
            _battleField.guid
        );
        if (skeletonDataAsset == null)
        {
            Debug.LogError($"BattleObjectFactory: Failed to load SkeletonDataAsset for {skinCfg.SpineRes}");
            return null;
        }
        // ==============================================
        GameObject battleGO = ResManager.Instance.LoadAsset<GameObject>("Hero/SpineRes", "Hero_001"/*skinCfg.SpineRes*/);
@@ -40,12 +54,6 @@
        GameObject realGO = GameObject.Instantiate(battleGO, goParent.transform);
        SkeletonAnimation skeletonAnimation = realGO.GetComponentInChildren<SkeletonAnimation>(true);
        var skeletonDataAsset = ResManager.Instance.LoadAsset<SkeletonDataAsset>("Hero/SpineRes/", skinCfg.SpineRes);
        if (skeletonDataAsset == null)
        {
            Debug.LogError("BattleObjectFactory.CreateBattleObject: skeletonDataAsset is null for " + skinCfg.SpineRes);
            return null;
        }
        float finalScaleRate = modelScaleRate * teamHero.modelScale;
Main/System/Battle/BattleResources.meta
New file
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: eb0c5385e2bb4aa4b8860b129609a2c4
folderAsset: yes
DefaultImporter:
  externalObjects: {}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/System/Battle/BattleResources/BattleAudioResLoader.cs
New file
@@ -0,0 +1,125 @@
using UnityEngine;
using System;
using System.Collections.Generic;
/// <summary>
/// 音频资源异步加载器
/// </summary>
public class BattleAudioResLoader
{
    private int loadingCount = 0;
    private int totalCount = 0;
    private Action<float> onProgress;
    private Action onComplete;
    private BattleCacheManager cacheManager;
    private bool isPersistent;
    /// <summary>
    /// 批量异步加载音频资源
    /// </summary>
    public void LoadAudioResourcesAsync(List<BattleResCache.ResourceIdentifier> identifiers,
        Dictionary<string, BattleResCache.CachedResource> cache,
        Action<float> progressCallback,
        Action completeCallback,
        BattleCacheManager manager = null,
        bool isRedTeam = false)
    {
        if (identifiers == null || identifiers.Count == 0)
        {
            completeCallback?.Invoke();
            return;
        }
        loadingCount = 0;
        totalCount = identifiers.Count;
        onProgress = progressCallback;
        onComplete = completeCallback;
        cacheManager = manager;
        isPersistent = isRedTeam;
        foreach (var identifier in identifiers)
        {
            string key = identifier.GetKey();
            // 检查缓存
            if (cache.ContainsKey(key))
            {
                // 已缓存,如果是红队资源且有管理器,添加引用
                if (isPersistent && cacheManager != null && !string.IsNullOrEmpty(identifier.OwnerId))
                {
                    cacheManager.AddRedTeamAudioReference(key, cache[key], identifier.OwnerId);
                }
                OnSingleLoadComplete();
                continue;
            }
            // 异步加载
            LoadSingleAudioAsync(identifier, cache);
        }
    }
    /// <summary>
    /// 加载单个音频资源
    /// </summary>
    private void LoadSingleAudioAsync(BattleResCache.ResourceIdentifier identifier,
        Dictionary<string, BattleResCache.CachedResource> cache)
    {
        ResManager.Instance.LoadAssetAsync<AudioClip>(
            identifier.Directory,
            identifier.AssetName,
            (success, asset) =>
            {
                if (success && asset != null)
                {
                    AudioClip audioClip = asset as AudioClip;
                    if (audioClip != null)
                    {
                        string key = identifier.GetKey();
                        var cachedRes = new BattleResCache.CachedResource(
                            identifier,
                            audioClip,
                            identifier.IsPersistent
                        );
                        cache[key] = cachedRes;
                        // 如果是红队资源且有管理器,添加引用
                        if (isPersistent && cacheManager != null && !string.IsNullOrEmpty(identifier.OwnerId))
                        {
                            cacheManager.AddRedTeamAudioReference(key, cachedRes, identifier.OwnerId);
                        }
                        Debug.Log($"BattleAudioResLoader: Loaded audio resource: {key}");
                    }
                    else
                    {
                        Debug.LogError($"BattleAudioResLoader: Failed to cast to AudioClip: {identifier.AssetName}");
                    }
                }
                else
                {
                    Debug.LogError($"BattleAudioResLoader: Failed to load audio resource: {identifier.Directory}/{identifier.AssetName}");
                }
                OnSingleLoadComplete();
            },
            false  // needExt 参数:音频文件名已包含扩展名
        );
    }
    /// <summary>
    /// 单个资源加载完成
    /// </summary>
    private void OnSingleLoadComplete()
    {
        loadingCount++;
        float progress = (float)loadingCount / totalCount;
        onProgress?.Invoke(progress);
        if (loadingCount >= totalCount)
        {
            Debug.Log($"BattleAudioResLoader: All audio resources loaded ({totalCount} items)");
            onComplete?.Invoke();
        }
    }
}
Main/System/Battle/BattleResources/BattleAudioResLoader.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ee1b6f0f511d6814b9d5d8c88ebbf62d
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/System/Battle/BattleResources/BattleCacheManager.cs
New file
@@ -0,0 +1,462 @@
using UnityEngine;
using System.Collections.Generic;
using Spine.Unity;
public class BattleCacheManager
{
    /// <summary>
    /// 资源引用信息
    /// </summary>
    private class ResourceReference
    {
        public BattleResCache.CachedResource CachedResource;
        public HashSet<string> OwnerIds = new HashSet<string>(); // 使用该资源的角色ID集合
        public int RefCount => OwnerIds.Count;
        public void AddOwner(string ownerId)
        {
            OwnerIds.Add(ownerId);
        }
        public void RemoveOwner(string ownerId)
        {
            OwnerIds.Remove(ownerId);
        }
    }
    // ===== 红队资源:全局共享,按引用计数管理 =====
    private static Dictionary<string, ResourceReference> globalRedTeamSpineCache =
        new Dictionary<string, ResourceReference>();
    private static Dictionary<string, ResourceReference> globalRedTeamAudioCache =
        new Dictionary<string, ResourceReference>();
    // ===== 蓝队资源:按战场GUID隔离 =====
    private static Dictionary<string, Dictionary<string, BattleResCache.CachedResource>> blueTeamSpineCacheDict =
        new Dictionary<string, Dictionary<string, BattleResCache.CachedResource>>();
    private static Dictionary<string, Dictionary<string, BattleResCache.CachedResource>> blueTeamAudioCacheDict =
        new Dictionary<string, Dictionary<string, BattleResCache.CachedResource>>();
    // 需要添加的字段
    private static Dictionary<string, HashSet<string>> battlefieldRedTeamOwners =
        new Dictionary<string, HashSet<string>>();  // <battleGuid, ownerIds>
    /// <summary>
    /// 获取Spine缓存(红队全局,蓝队按战场隔离)
    /// </summary>
    public Dictionary<string, BattleResCache.CachedResource> GetSpineCache(bool isPersistent, string battleGuid = "")
    {
        if (isPersistent)
        {
            // 红队:将引用字典转换为普通缓存字典(兼容加载器)
            var cache = new Dictionary<string, BattleResCache.CachedResource>();
            foreach (var kvp in globalRedTeamSpineCache)
            {
                cache[kvp.Key] = kvp.Value.CachedResource;
            }
            return cache;
        }
        else
        {
            // 蓝队:返回战场专属缓存
            if (!blueTeamSpineCacheDict.ContainsKey(battleGuid))
            {
                blueTeamSpineCacheDict[battleGuid] = new Dictionary<string, BattleResCache.CachedResource>();
            }
            return blueTeamSpineCacheDict[battleGuid];
        }
    }
    /// <summary>
    /// 获取音频缓存(红队全局,蓝队按战场隔离)
    /// </summary>
    public Dictionary<string, BattleResCache.CachedResource> GetAudioCache(bool isPersistent, string battleGuid = "")
    {
        if (isPersistent)
        {
            // 红队:将引用字典转换为普通缓存字典
            var cache = new Dictionary<string, BattleResCache.CachedResource>();
            foreach (var kvp in globalRedTeamAudioCache)
            {
                cache[kvp.Key] = kvp.Value.CachedResource;
            }
            return cache;
        }
        else
        {
            // 蓝队:返回战场专属缓存
            if (!blueTeamAudioCacheDict.ContainsKey(battleGuid))
            {
                blueTeamAudioCacheDict[battleGuid] = new Dictionary<string, BattleResCache.CachedResource>();
            }
            return blueTeamAudioCacheDict[battleGuid];
        }
    }
    /// <summary>
    /// 添加红队资源引用(由加载器调用)
    /// </summary>
    public void AddRedTeamSpineReference(string key, BattleResCache.CachedResource resource, string ownerId)
    {
        if (!globalRedTeamSpineCache.ContainsKey(key))
        {
            globalRedTeamSpineCache[key] = new ResourceReference
            {
                CachedResource = resource
            };
        }
        globalRedTeamSpineCache[key].AddOwner(ownerId);
    }
    /// <summary>
    /// 添加红队音频引用
    /// </summary>
    public void AddRedTeamAudioReference(string key, BattleResCache.CachedResource resource, string ownerId)
    {
        if (!globalRedTeamAudioCache.ContainsKey(key))
        {
            globalRedTeamAudioCache[key] = new ResourceReference
            {
                CachedResource = resource
            };
        }
        globalRedTeamAudioCache[key].AddOwner(ownerId);
    }
    /// <summary>
    /// 获取Spine资源(未命中时自动加载并缓存)
    /// </summary>
    public SkeletonDataAsset GetSpineResource(string directory, string assetName, string battleGuid = "", bool autoLoadIfMissing = true)
    {
        string key = $"{directory}/{assetName}";
        // 优先从红队全局缓存查找
        if (globalRedTeamSpineCache.TryGetValue(key, out var redRef))
        {
            return redRef.CachedResource.Asset as SkeletonDataAsset;
        }
        // 再从蓝队战场专属缓存查找
        if (!string.IsNullOrEmpty(battleGuid) && blueTeamSpineCacheDict.TryGetValue(battleGuid, out var blueCache))
        {
            if (blueCache.TryGetValue(key, out var blueRes))
            {
                return blueRes.Asset as SkeletonDataAsset;
            }
        }
        // ===== 缓存未命中时自动加载 =====
        if (autoLoadIfMissing)
        {
            Debug.LogWarning($"BattleCacheManager: Spine cache miss for {key}, loading on-demand...");
            SkeletonDataAsset asset = ResManager.Instance.LoadAsset<SkeletonDataAsset>(directory, assetName);
            if (asset != null)
            {
                var identifier = new BattleResCache.ResourceIdentifier
                {
                    Directory = directory,
                    AssetName = assetName,
                    Type = BattleResCache.ResourceType.Spine,
                    IsPersistent = string.IsNullOrEmpty(battleGuid)
                };
                var cachedRes = new BattleResCache.CachedResource(identifier, asset, identifier.IsPersistent);
                if (string.IsNullOrEmpty(battleGuid))
                {
                    // 红队:添加引用(未知所有者,用特殊标识)
                    AddRedTeamSpineReference(key, cachedRes, "OnDemand");
                    Debug.Log($"BattleCacheManager: Added to global red cache: {key}");
                }
                else
                {
                    // 蓝队:直接加入战场缓存
                    if (!blueTeamSpineCacheDict.ContainsKey(battleGuid))
                    {
                        blueTeamSpineCacheDict[battleGuid] = new Dictionary<string, BattleResCache.CachedResource>();
                    }
                    blueTeamSpineCacheDict[battleGuid][key] = cachedRes;
                    Debug.Log($"BattleCacheManager: Added to blue cache (BF={battleGuid}): {key}");
                }
                return asset;
            }
        }
        return null;
    }
    /// <summary>
    /// 获取音频资源(未命中时自动加载并缓存)
    /// </summary>
    public AudioClip GetAudioResource(string directory, string assetName, string battleGuid = "", bool autoLoadIfMissing = true)
    {
        string key = $"{directory}/{assetName}";
        // 优先从红队全局缓存查找
        if (globalRedTeamAudioCache.TryGetValue(key, out var redRef))
        {
            return redRef.CachedResource.Asset as AudioClip;
        }
        // 再从蓝队战场专属缓存查找
        if (!string.IsNullOrEmpty(battleGuid) && blueTeamAudioCacheDict.TryGetValue(battleGuid, out var blueCache))
        {
            if (blueCache.TryGetValue(key, out var blueRes))
            {
                return blueRes.Asset as AudioClip;
            }
        }
        // ===== 缓存未命中时自动加载 =====
        if (autoLoadIfMissing)
        {
            Debug.LogWarning($"BattleCacheManager: Audio cache miss for {key}, loading on-demand...");
            AudioClip asset = ResManager.Instance.LoadAsset<AudioClip>(directory, assetName, false);
            if (asset != null)
            {
                var identifier = new BattleResCache.ResourceIdentifier
                {
                    Directory = directory,
                    AssetName = assetName,
                    Type = BattleResCache.ResourceType.Audio,
                    IsPersistent = string.IsNullOrEmpty(battleGuid)
                };
                var cachedRes = new BattleResCache.CachedResource(identifier, asset, identifier.IsPersistent);
                if (string.IsNullOrEmpty(battleGuid))
                {
                    // 红队:添加引用
                    AddRedTeamAudioReference(key, cachedRes, "OnDemand");
                    Debug.Log($"BattleCacheManager: Added to global red audio cache: {key}");
                }
                else
                {
                    // 蓝队:直接加入战场缓存
                    if (!blueTeamAudioCacheDict.ContainsKey(battleGuid))
                    {
                        blueTeamAudioCacheDict[battleGuid] = new Dictionary<string, BattleResCache.CachedResource>();
                    }
                    blueTeamAudioCacheDict[battleGuid][key] = cachedRes;
                    Debug.Log($"BattleCacheManager: Added to blue audio cache (BF={battleGuid}): {key}");
                }
                return asset;
            }
        }
        return null;
    }
    /// <summary>
    /// 移除指定角色的红队资源引用
    /// </summary>
    public void RemoveRedTeamReferences(List<string> ownerIds)
    {
        if (ownerIds == null || ownerIds.Count == 0)
            return;
        int removedSpineCount = 0;
        int removedAudioCount = 0;
        // 处理Spine资源
        var spineKeysToRemove = new List<string>();
        foreach (var kvp in globalRedTeamSpineCache)
        {
            foreach (var ownerId in ownerIds)
            {
                kvp.Value.RemoveOwner(ownerId);
            }
            // 如果没有引用了,标记删除
            if (kvp.Value.RefCount == 0)
            {
                spineKeysToRemove.Add(kvp.Key);
            }
        }
        foreach (var key in spineKeysToRemove)
        {
            var resource = globalRedTeamSpineCache[key].CachedResource;
            ResManager.Instance.UnloadAsset(
                resource.Identifier.Directory.ToLower(),
                resource.Identifier.AssetName.ToLower()
            );
            globalRedTeamSpineCache.Remove(key);
            removedSpineCount++;
        }
        // 处理音频资源
        var audioKeysToRemove = new List<string>();
        foreach (var kvp in globalRedTeamAudioCache)
        {
            foreach (var ownerId in ownerIds)
            {
                kvp.Value.RemoveOwner(ownerId);
            }
            if (kvp.Value.RefCount == 0)
            {
                audioKeysToRemove.Add(kvp.Key);
            }
        }
        foreach (var key in audioKeysToRemove)
        {
            var resource = globalRedTeamAudioCache[key].CachedResource;
            ResManager.Instance.UnloadAsset(
                resource.Identifier.Directory.ToLower(),
                resource.Identifier.AssetName.ToLower()
            );
            globalRedTeamAudioCache.Remove(key);
            removedAudioCount++;
        }
        Debug.Log($"BattleCacheManager: Removed {ownerIds.Count} owner(s), freed {removedSpineCount} spine + {removedAudioCount} audio resources");
    }
    /// <summary>
    /// 清空指定战场的蓝队缓存
    /// </summary>
    public void ClearBlueTeamCache(string battleGuid)
    {
        if (blueTeamSpineCacheDict.ContainsKey(battleGuid))
        {
            blueTeamSpineCacheDict.Remove(battleGuid);
        }
        if (blueTeamAudioCacheDict.ContainsKey(battleGuid))
        {
            blueTeamAudioCacheDict.Remove(battleGuid);
        }
        Debug.Log($"BattleCacheManager: Cleared blue team cache for battlefield {battleGuid}");
    }
    /// <summary>
    /// 清空红队全局缓存(玩家重置阵容时调用)
    /// </summary>
    public void ClearRedTeamCache()
    {
        globalRedTeamSpineCache.Clear();
        globalRedTeamAudioCache.Clear();
        Debug.Log("BattleCacheManager: Cleared red team global cache");
    }
    /// <summary>
    /// 获取缓存统计信息
    /// </summary>
    public string GetCacheStats(string battleGuid = "")
    {
        int blueSpineCount = blueTeamSpineCacheDict.ContainsKey(battleGuid) ? blueTeamSpineCacheDict[battleGuid].Count : 0;
        int blueAudioCount = blueTeamAudioCacheDict.ContainsKey(battleGuid) ? blueTeamAudioCacheDict[battleGuid].Count : 0;
        return $"Red Spine: {globalRedTeamSpineCache.Count}, Red Audio: {globalRedTeamAudioCache.Count}, " +
               $"Blue Spine (BF={battleGuid}): {blueSpineCount}, Blue Audio (BF={battleGuid}): {blueAudioCount}";
    }
    // ========== BattleCacheManager.cs 新增方法 ==========
    /// <summary>
    /// 记录战场的红队资源需求(增加引用)
    /// </summary>
    public void RegisterBattlefieldRedTeam(string battleGuid, List<BattleResCache.ResourceIdentifier> spineResources, List<BattleResCache.ResourceIdentifier> audioResources)
    {
        // 记录这个战场使用的红队资源的OwnerIds
        var ownerIds = new HashSet<string>();
        foreach (var res in spineResources)
        {
            if (!string.IsNullOrEmpty(res.OwnerId))
            {
                ownerIds.Add(res.OwnerId);
            }
        }
        foreach (var res in audioResources)
        {
            if (!string.IsNullOrEmpty(res.OwnerId))
            {
                ownerIds.Add(res.OwnerId);
            }
        }
        if (!battlefieldRedTeamOwners.ContainsKey(battleGuid))
        {
            battlefieldRedTeamOwners[battleGuid] = ownerIds;
        }
    }
    /// <summary>
    /// 注销战场的红队资源需求(减少引用)
    /// </summary>
    public void UnregisterBattlefieldRedTeam(string battleGuid)
    {
        if (!battlefieldRedTeamOwners.ContainsKey(battleGuid))
            return;
        var ownerIds = battlefieldRedTeamOwners[battleGuid];
        // 从所有红队资源中移除这些OwnerIds的引用
        RemoveOwnersFromRedTeamCache(ownerIds);
        battlefieldRedTeamOwners.Remove(battleGuid);
    }
    private void RemoveOwnersFromRedTeamCache(HashSet<string> ownerIds)
    {
        // 处理Spine资源
        var spineKeysToRemove = new List<string>();
        foreach (var kvp in globalRedTeamSpineCache)
        {
            foreach (var ownerId in ownerIds)
            {
                kvp.Value.RemoveOwner(ownerId);
            }
            // 引用计数为0时真正卸载
            if (kvp.Value.RefCount == 0)
            {
                spineKeysToRemove.Add(kvp.Key);
            }
        }
        foreach (var key in spineKeysToRemove)
        {
            var res = globalRedTeamSpineCache[key];
            ResManager.Instance.UnloadAsset(res.CachedResource.Identifier.Directory, res.CachedResource.Identifier.AssetName);
            globalRedTeamSpineCache.Remove(key);
            Debug.Log($"BattleCacheManager: Unloaded red team spine (refCount=0): {key}");
        }
        // 处理Audio资源
        var audioKeysToRemove = new List<string>();
        foreach (var kvp in globalRedTeamAudioCache)
        {
            foreach (var ownerId in ownerIds)
            {
                kvp.Value.RemoveOwner(ownerId);
            }
            if (kvp.Value.RefCount == 0)
            {
                audioKeysToRemove.Add(kvp.Key);
            }
        }
        foreach (var key in audioKeysToRemove)
        {
            var res = globalRedTeamAudioCache[key];
            ResManager.Instance.UnloadAsset(res.CachedResource.Identifier.Directory, res.CachedResource.Identifier.AssetName);
            globalRedTeamAudioCache.Remove(key);
            Debug.Log($"BattleCacheManager: Unloaded red team audio (refCount=0): {key}");
        }
    }
}
Main/System/Battle/BattleResources/BattleCacheManager.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 257fb2e8a69b6a44899a0111603f9fdc
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/System/Battle/BattleResources/BattlePreloadManager.cs
New file
@@ -0,0 +1,168 @@
using UnityEngine;
using System;
using System.Collections.Generic;
public class BattlePreloadManager
{
    private BattleSpineResLoader spineLoader = new BattleSpineResLoader();
    private BattleAudioResLoader audioLoader = new BattleAudioResLoader();
    private BattleCacheManager cacheManager = new BattleCacheManager();
    private BattleUnloadManager unloadManager = new BattleUnloadManager();
    private bool isLoading = false;
    public BattleCacheManager CacheManager => cacheManager;
    public BattleUnloadManager UnloadManager => unloadManager;
    /// <summary>
    /// 预加载战斗资源
    /// </summary>
    public void PreloadBattleResources(string battleGuid, List<TeamBase> redTeamList, List<TeamBase> blueTeamList,
        Action<float> progressCallback, Action completeCallback)
    {
        if (isLoading)
        {
            Debug.LogWarning("BattlePreloadManager: Already loading, ignoring request");
            return;
        }
        isLoading = true;
        var redTeamInfo = AnalyzeTeamList(redTeamList, true);
        var blueTeamInfo = AnalyzeTeamList(blueTeamList, false);
        // ===== 新增:注册战场红队资源需求 =====
        cacheManager.RegisterBattlefieldRedTeam(battleGuid, redTeamInfo.SpineResources, redTeamInfo.AudioResources);
        StartPreload(redTeamInfo, blueTeamInfo, battleGuid, progressCallback, () =>
        {
            isLoading = false;
            completeCallback?.Invoke();
        });
    }
    private TeamResTracker.TeamResourceInfo AnalyzeTeamList(List<TeamBase> teamList, bool isPersistent)
    {
        var combinedInfo = new TeamResTracker.TeamResourceInfo();
        if (teamList == null || teamList.Count == 0)
        {
            return combinedInfo;
        }
        foreach (var team in teamList)
        {
            if (team == null)
                continue;
            var teamInfo = TeamResTracker.AnalyzeTeam(team, isPersistent);
            MergeResourceInfo(combinedInfo, teamInfo);
        }
        return combinedInfo;
    }
    private void MergeResourceInfo(TeamResTracker.TeamResourceInfo target, TeamResTracker.TeamResourceInfo source)
    {
        // 合并Spine资源(去重)
        foreach (var res in source.SpineResources)
        {
            if (!ContainsResource(target.SpineResources, res))
            {
                target.SpineResources.Add(res);
            }
        }
        // 合并音频资源(去重)
        foreach (var res in source.AudioResources)
        {
            if (!ContainsResource(target.AudioResources, res))
            {
                target.AudioResources.Add(res);
            }
        }
    }
    private bool ContainsResource(List<BattleResCache.ResourceIdentifier> list, BattleResCache.ResourceIdentifier resource)
    {
        foreach (var item in list)
        {
            if (item.GetKey() == resource.GetKey())
            {
                return true;
            }
        }
        return false;
    }
    private void StartPreload(TeamResTracker.TeamResourceInfo redInfo, TeamResTracker.TeamResourceInfo blueInfo,
        string battleGuid,
        Action<float> progressCallback, Action completeCallback)
    {
        int totalResources = redInfo.GetTotalCount() + blueInfo.GetTotalCount();
        if (totalResources == 0)
        {
            Debug.Log("BattlePreloadManager: No resources to preload");
            completeCallback?.Invoke();
            return;
        }
        Debug.Log($"BattlePreloadManager: Preloading {totalResources} resources for battlefield {battleGuid}");
        Debug.Log($"  Red: Spine={redInfo.SpineResources.Count}, Audio={redInfo.AudioResources.Count}");
        Debug.Log($"  Blue: Spine={blueInfo.SpineResources.Count}, Audio={blueInfo.AudioResources.Count}");
        int completedPhases = 0;
        int totalPhases = 4;
        Action onPhaseComplete = () =>
        {
            completedPhases++;
            float progress = (float)completedPhases / totalPhases;
            progressCallback?.Invoke(progress);
            if (completedPhases >= totalPhases)
            {
                Debug.Log($"BattlePreloadManager: Completed! {cacheManager.GetCacheStats(battleGuid)}");
                completeCallback?.Invoke();
            }
        };
        // 并行加载4个阶段(传入cacheManager和是否为红队标识)
        spineLoader.LoadSpineResourcesAsync(
            redInfo.SpineResources,
            cacheManager.GetSpineCache(true, battleGuid),
            null,
            onPhaseComplete,
            cacheManager,  // ← 传入管理器
            true           // ← 是红队
        );
        audioLoader.LoadAudioResourcesAsync(
            redInfo.AudioResources,
            cacheManager.GetAudioCache(true, battleGuid),
            null,
            onPhaseComplete,
            cacheManager,  // ← 传入管理器
            true           // ← 是红队
        );
        spineLoader.LoadSpineResourcesAsync(
            blueInfo.SpineResources,
            cacheManager.GetSpineCache(false, battleGuid),
            null,
            onPhaseComplete,
            null,   // ← 蓝队不需要引用追踪
            false   // ← 不是红队
        );
        audioLoader.LoadAudioResourcesAsync(
            blueInfo.AudioResources,
            cacheManager.GetAudioCache(false, battleGuid),
            null,
            onPhaseComplete,
            null,   // ← 蓝队不需要引用追踪
            false   // ← 不是红队
        );
    }
}
Main/System/Battle/BattleResources/BattlePreloadManager.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e34093150a553f343ac1758dd429544a
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/System/Battle/BattleResources/BattleResCache.cs
New file
@@ -0,0 +1,93 @@
using UnityEngine;
using Spine.Unity;
using System.Collections.Generic;
/// <summary>
/// 战斗资源缓存信息
/// </summary>
public class BattleResCache
{
    /// <summary>
    /// 资源标识符
    /// </summary>
    public class ResourceIdentifier
    {
        public string Directory;      // 资源目录
        public string AssetName;      // 资源名称
        public ResourceType Type;     // 资源类型
        public bool IsPersistent;     // 是否常驻(红队资源)
        public string OwnerId;        // 资源所有者ID (格式: HeroID_SkinID)
        /// <summary>
        /// 获取资源唯一Key(目录+名称)
        /// </summary>
        public string GetKey()
        {
            return $"{Directory}/{AssetName}";
        }
        public override int GetHashCode()
        {
            return GetKey().GetHashCode();
        }
        public override bool Equals(object obj)
        {
            if (obj is ResourceIdentifier other)
            {
                return GetKey() == other.GetKey();
            }
            return false;
        }
    }
    /// <summary>
    /// 资源类型
    /// </summary>
    public enum ResourceType
    {
        Spine,      // Spine动画资源
        Audio       // 音频资源
    }
    /// <summary>
    /// 缓存的资源项
    /// </summary>
    public class CachedResource
    {
        public ResourceIdentifier Identifier;
        public Object Asset;              // UnityEngine.Object
        public bool IsPersistent;         // 是否常驻
        public CachedResource(ResourceIdentifier identifier, Object asset, bool isPersistent)
        {
            Identifier = identifier;
            Asset = asset;
            IsPersistent = isPersistent;
        }
        /// <summary>
        /// 是否可以卸载(非常驻资源才能卸载)
        /// </summary>
        public bool CanUnload()
        {
            return !IsPersistent;
        }
        /// <summary>
        /// 获取Spine资源
        /// </summary>
        public SkeletonDataAsset GetSkeletonDataAsset()
        {
            return Asset as SkeletonDataAsset;
        }
        /// <summary>
        /// 获取音频资源
        /// </summary>
        public AudioClip GetAudioClip()
        {
            return Asset as AudioClip;
        }
    }
}
Main/System/Battle/BattleResources/BattleResCache.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 57597e8314173bd498166dc244e55a96
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/System/Battle/BattleResources/BattleResManager.cs
New file
@@ -0,0 +1,54 @@
using UnityEngine;
using System;
using System.Collections.Generic;
using Spine.Unity;
/// <summary>
/// 战斗资源总管理器
/// 对外提供统一的资源管理接口
/// </summary>
public class BattleResManager : Singleton<BattleResManager>
{
    private BattlePreloadManager preloadManager = new BattlePreloadManager();
    /// <summary>
    /// 预加载战斗资源
    /// </summary>
    public void PreloadBattleResources(string battleGuid, List<TeamBase> redTeamList, List<TeamBase> blueTeamList,
        Action<float> progressCallback = null, Action completeCallback = null)
    {
        preloadManager.PreloadBattleResources(battleGuid, redTeamList, blueTeamList, progressCallback, completeCallback);
    }
    /// <summary>
    /// 战斗结束后卸载蓝队资源
    /// </summary>
    public void UnloadBattleResources(string battleGuid)
    {
        preloadManager.UnloadManager.UnloadBlueTeamResources(preloadManager.CacheManager, battleGuid);
    }
    /// <summary>
    /// 获取Spine资源
    /// </summary>
    public SkeletonDataAsset GetSpineResource(string directory, string assetName, string battleGuid = "", bool autoLoadIfMissing = true)
    {
        return preloadManager.CacheManager.GetSpineResource(directory, assetName, battleGuid, autoLoadIfMissing);
    }
    /// <summary>
    /// 获取音频资源
    /// </summary>
    public AudioClip GetAudioResource(string directory, string assetName, string battleGuid = "", bool autoLoadIfMissing = true)
    {
        return preloadManager.CacheManager.GetAudioResource(directory, assetName, battleGuid, autoLoadIfMissing);
    }
    /// <summary>
    /// 处理红队变更
    /// </summary>
    public void HandleRedTeamChange(List<TeamBase> newRedTeamList, Action completeCallback = null)
    {
        preloadManager.HandleRedTeamChange(newRedTeamList, completeCallback);
    }
}
Main/System/Battle/BattleResources/BattleResManager.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b625f831b0c2aa0439ba1614b4159616
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/System/Battle/BattleResources/BattleSpineResLoader.cs
New file
@@ -0,0 +1,125 @@
using UnityEngine;
using Spine.Unity;
using System;
using System.Collections.Generic;
/// <summary>
/// Spine资源异步加载器
/// </summary>
public class BattleSpineResLoader
{
    private int loadingCount = 0;
    private int totalCount = 0;
    private Action<float> onProgress;
    private Action onComplete;
    private BattleCacheManager cacheManager;
    private bool isPersistent;
    /// <summary>
    /// 批量异步加载Spine资源
    /// </summary>
    public void LoadSpineResourcesAsync(List<BattleResCache.ResourceIdentifier> identifiers,
        Dictionary<string, BattleResCache.CachedResource> cache,
        Action<float> progressCallback,
        Action completeCallback,
        BattleCacheManager manager = null,
        bool isRedTeam = false)
    {
        if (identifiers == null || identifiers.Count == 0)
        {
            completeCallback?.Invoke();
            return;
        }
        loadingCount = 0;
        totalCount = identifiers.Count;
        onProgress = progressCallback;
        onComplete = completeCallback;
        cacheManager = manager;
        isPersistent = isRedTeam;
        foreach (var identifier in identifiers)
        {
            string key = identifier.GetKey();
            // 检查缓存
            if (cache.ContainsKey(key))
            {
                // 已缓存,如果是红队资源且有管理器,添加引用
                if (isPersistent && cacheManager != null && !string.IsNullOrEmpty(identifier.OwnerId))
                {
                    cacheManager.AddRedTeamSpineReference(key, cache[key], identifier.OwnerId);
                }
                OnSingleLoadComplete();
                continue;
            }
            // 异步加载
            LoadSingleSpineAsync(identifier, cache);
        }
    }
    /// <summary>
    /// 加载单个Spine资源
    /// </summary>
    private void LoadSingleSpineAsync(BattleResCache.ResourceIdentifier identifier,
        Dictionary<string, BattleResCache.CachedResource> cache)
    {
        ResManager.Instance.LoadAssetAsync<SkeletonDataAsset>(
            identifier.Directory,
            identifier.AssetName,
            (success, asset) =>
            {
                if (success && asset != null)
                {
                    SkeletonDataAsset skeletonAsset = asset as SkeletonDataAsset;
                    if (skeletonAsset != null)
                    {
                        string key = identifier.GetKey();
                        var cachedRes = new BattleResCache.CachedResource(
                            identifier,
                            skeletonAsset,
                            identifier.IsPersistent
                        );
                        cache[key] = cachedRes;
                        // 如果是红队资源且有管理器,添加引用
                        if (isPersistent && cacheManager != null && !string.IsNullOrEmpty(identifier.OwnerId))
                        {
                            cacheManager.AddRedTeamSpineReference(key, cachedRes, identifier.OwnerId);
                        }
                        Debug.Log($"BattleSpineResLoader: Loaded spine resource: {key}");
                    }
                    else
                    {
                        Debug.LogError($"BattleSpineResLoader: Failed to cast to SkeletonDataAsset: {identifier.AssetName}");
                    }
                }
                else
                {
                    Debug.LogError($"BattleSpineResLoader: Failed to load spine resource: {identifier.Directory}/{identifier.AssetName}");
                }
                OnSingleLoadComplete();
            }
        );
    }
    /// <summary>
    /// 单个资源加载完成
    /// </summary>
    private void OnSingleLoadComplete()
    {
        loadingCount++;
        float progress = (float)loadingCount / totalCount;
        onProgress?.Invoke(progress);
        if (loadingCount >= totalCount)
        {
            Debug.Log($"BattleSpineResLoader: All Spine resources loaded ({totalCount} items)");
            onComplete?.Invoke();
        }
    }
}
Main/System/Battle/BattleResources/BattleSpineResLoader.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b7c61553574c7a045871fc98457bfe1a
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/System/Battle/BattleResources/BattleUnloadManager.cs
New file
@@ -0,0 +1,103 @@
using UnityEngine;
using System.Collections.Generic;
public class BattleUnloadManager
{
    /// <summary>
    /// 卸载指定战场的蓝队资源
    /// </summary>
    public void UnloadBlueTeamResources(BattleCacheManager cacheManager, string battleGuid)
    {
        var blueSpineCache = cacheManager.GetSpineCache(false, battleGuid);
        var blueAudioCache = cacheManager.GetAudioCache(false, battleGuid);
        int spineCount = UnloadResourceCache(blueSpineCache);
        int audioCount = UnloadResourceCache(blueAudioCache);
        cacheManager.ClearBlueTeamCache(battleGuid);
        Debug.Log($"BattleUnloadManager: Unloaded blue team for battlefield {battleGuid} - Spine: {spineCount}, Audio: {audioCount}");
    }
    /// <summary>
    /// 卸载红队资源(队伍变更时使用)
    /// </summary>
    public void UnloadRedTeamResources(BattleCacheManager cacheManager)
    {
        var redSpineCache = cacheManager.GetSpineCache(true, "");
        var redAudioCache = cacheManager.GetAudioCache(true, "");
        int spineCount = UnloadResourceCache(redSpineCache);
        int audioCount = UnloadResourceCache(redAudioCache);
        cacheManager.ClearRedTeamCache();
        Debug.Log($"BattleUnloadManager: Unloaded red team - Spine: {spineCount}, Audio: {audioCount}");
    }
    /// <summary>
    /// 卸载整个资源缓存
    /// </summary>
    private int UnloadResourceCache(Dictionary<string, BattleResCache.CachedResource> cache)
    {
        int unloadCount = 0;
        foreach (var kvp in cache)
        {
            if (kvp.Value.CanUnload())
            {
                UnloadSingleResource(kvp.Value);
                unloadCount++;
            }
        }
        return unloadCount;
    }
    /// <summary>
    /// 卸载单个资源
    /// </summary>
    private void UnloadSingleResource(BattleResCache.CachedResource cachedRes)
    {
        if (cachedRes == null || cachedRes.Asset == null)
        {
            return;
        }
        string assetKey = cachedRes.Identifier.GetKey();
        // 卸载资源
        ResManager.Instance.UnloadAsset(
            cachedRes.Identifier.Directory.ToLower(),
            cachedRes.Identifier.AssetName.ToLower()
        );
        Debug.Log($"BattleUnloadManager: Unloaded resource: {assetKey}");
    }
    /// <summary>
    /// 卸载所有资源
    /// </summary>
    public void UnloadAllResources(BattleCacheManager cacheManager, string battleGuid = "")
    {
        UnloadRedTeamResources(cacheManager);
        if (!string.IsNullOrEmpty(battleGuid))
        {
            UnloadBlueTeamResources(cacheManager, battleGuid);
        }
        Debug.Log("BattleUnloadManager: All resources unloaded");
    }
    /// <summary>
    /// 卸载战场资源
    /// </summary>
    public void UnloadBattleResources(BattleCacheManager cacheManager, string battleGuid)
    {
        // 卸载蓝队资源
        UnloadBlueTeamResources(cacheManager, battleGuid);
        // ===== 新增:注销战场红队引用(自动按引用计数卸载)=====
        cacheManager.UnregisterBattlefieldRedTeam(battleGuid);
    }
}
Main/System/Battle/BattleResources/BattleUnloadManager.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f103af01d502fa142b50f08dc14cdb67
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/System/Battle/BattleResources/TeamResTracker.cs
New file
@@ -0,0 +1,192 @@
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 队伍资源追踪器
/// 分析队伍需要哪些资源(Spine动画、音频)
/// </summary>
public class TeamResTracker
{
    /// <summary>
    /// 队伍资源信息
    /// </summary>
    public class TeamResourceInfo
    {
        public List<BattleResCache.ResourceIdentifier> SpineResources = new List<BattleResCache.ResourceIdentifier>();
        public List<BattleResCache.ResourceIdentifier> AudioResources = new List<BattleResCache.ResourceIdentifier>();
        public void Clear()
        {
            SpineResources.Clear();
            AudioResources.Clear();
        }
        /// <summary>
        /// 获取所有资源数量
        /// </summary>
        public int GetTotalCount()
        {
            return SpineResources.Count + AudioResources.Count;
        }
    }
    /// <summary>
    /// 分析单个队伍的资源需求
    /// </summary>
    public static TeamResourceInfo AnalyzeTeam(TeamBase team, bool isPersistent)
    {
        TeamResourceInfo info = new TeamResourceInfo();
        if (team == null || team.serverHeroes == null)
        {
            return info;
        }
        foreach (var teamHero in team.serverHeroes)
        {
            if (teamHero == null)
                continue;
            // 生成角色唯一标识(用于引用追踪)
            string ownerId = $"{teamHero.heroId}_{teamHero.SkinID}";
            // ===== 移除:不再预加载角色Spine资源 =====
            // AddHeroSpineResource(teamHero, info, isPersistent, ownerId);
            // 只预加载技能相关资源(音效、特效Spine)
            AddSkillResources(teamHero, info, isPersistent, ownerId);
        }
        return info;
    }
    /// <summary>
    /// 添加技能相关资源
    /// </summary>
    private static void AddSkillResources(TeamHero teamHero, TeamResourceInfo info, bool isPersistent, string ownerId)
    {
        if (teamHero.heroConfig == null)
            return;
        // 普攻技能
        if (teamHero.heroConfig.AtkSkillID > 0)
        {
            AddSingleSkillResources(teamHero.heroConfig.AtkSkillID, info, isPersistent, ownerId);
        }
        // 怒气技能
        if (teamHero.heroConfig.AngerSkillID > 0)
        {
            AddSingleSkillResources(teamHero.heroConfig.AngerSkillID, info, isPersistent, ownerId);
        }
    }
    /// <summary>
    /// 添加单个技能的资源
    /// </summary>
    private static void AddSingleSkillResources(int skillId, TeamResourceInfo info, bool isPersistent, string ownerId)
    {
        SkillConfig skillConfig = SkillConfig.Get(skillId);
        if (skillConfig == null)
            return;
        AddSkillAudio(skillConfig, info, isPersistent, ownerId);
        AddSkillEffects(skillConfig, info, isPersistent, ownerId);
    }
    private static void AddSkillAudio(SkillConfig skillConfig, TeamResourceInfo info, bool isPersistent, string ownerId)
    {
        if (skillConfig.SkinllSFX1 > 0)
        {
            AddAudioResource(skillConfig.SkinllSFX1, info, isPersistent, ownerId);
        }
        if (skillConfig.SkinllSFX2 > 0)
        {
            AddAudioResource(skillConfig.SkinllSFX2, info, isPersistent, ownerId);
        }
    }
    private static void AddSkillEffects(SkillConfig skillConfig, TeamResourceInfo info, bool isPersistent, string ownerId)
    {
        List<int> effectIds = new List<int>();
        if (skillConfig.BulletEffectId > 0) effectIds.Add(skillConfig.BulletEffectId);
        if (skillConfig.ExplosionEffectId > 0) effectIds.Add(skillConfig.ExplosionEffectId);
        if (skillConfig.ExplosionEffect2 > 0) effectIds.Add(skillConfig.ExplosionEffect2);
        if (skillConfig.ExplosionEffect3 > 0) effectIds.Add(skillConfig.ExplosionEffect3);
        if (skillConfig.ExplosionEffect4 > 0) effectIds.Add(skillConfig.ExplosionEffect4);
        if (skillConfig.EffectId > 0) effectIds.Add(skillConfig.EffectId);
        if (skillConfig.EffectId2 > 0) effectIds.Add(skillConfig.EffectId2);
        if (skillConfig.MStartEffectId > 0) effectIds.Add(skillConfig.MStartEffectId);
        if (skillConfig.BuffEffect > 0) effectIds.Add(skillConfig.BuffEffect);
        if (skillConfig.TriggerEffect > 0) effectIds.Add(skillConfig.TriggerEffect);
        foreach (int effectId in effectIds)
        {
            EffectConfig effectConfig = EffectConfig.Get(effectId);
            if (effectConfig == null)
                continue;
            // 特效Spine资源(只预加载特效Spine,不预加载角色Spine)
            if (effectConfig.isSpine > 0 && !string.IsNullOrEmpty(effectConfig.fxName))
            {
                var identifier = new BattleResCache.ResourceIdentifier
                {
                    Directory = "UIEffect/" + effectConfig.packageName,
                    AssetName = effectConfig.fxName,
                    Type = BattleResCache.ResourceType.Spine,
                    IsPersistent = isPersistent,
                    OwnerId = ownerId  // ← 添加所有者ID
                };
                if (!ContainsResource(info.SpineResources, identifier))
                {
                    info.SpineResources.Add(identifier);
                }
            }
            // 特效音频
            if (effectConfig.audio > 0)
            {
                AddAudioResource(effectConfig.audio, info, isPersistent, ownerId);
            }
        }
    }
    private static void AddAudioResource(int audioId, TeamResourceInfo info, bool isPersistent, string ownerId)
    {
        AudioConfig audioConfig = AudioConfig.Get(audioId);
        if (audioConfig == null)
            return;
        var identifier = new BattleResCache.ResourceIdentifier
        {
            Directory = "Audio/" + audioConfig.Folder,  // ← 修复:添加 Audio/ 前缀
            AssetName = audioConfig.Audio,
            Type = BattleResCache.ResourceType.Audio,
            IsPersistent = isPersistent,
            OwnerId = ownerId
        };
        if (!ContainsResource(info.AudioResources, identifier))
        {
            info.AudioResources.Add(identifier);
        }
    }
    /// <summary>
    /// 检查资源列表是否包含指定资源
    /// </summary>
    private static bool ContainsResource(List<BattleResCache.ResourceIdentifier> list, BattleResCache.ResourceIdentifier resource)
    {
        foreach (var item in list)
        {
            if (item.GetKey() == resource.GetKey())
            {
                return true;
            }
        }
        return false;
    }
}
Main/System/Battle/BattleResources/TeamResTracker.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1ab0650d593061a4aaa2e7ad15d86d10
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/System/Battle/RecordPlayer/RecordActionType.cs
@@ -12,4 +12,6 @@
    DodgeFinish,//闪避完成
    RoundChange,//回合切换
    PreloadRes,//预加载资源
}
Main/System/Battle/Sound/BattleSoundManager.cs
@@ -195,11 +195,18 @@
    private AudioClip LoadAudioClip(int audioId)
    {
        var config = AudioConfig.Get(audioId);
        if (config != null)
        {
            return AudioLoader.LoadAudio(config.Folder, config.Audio);
        }
        return null;
        if (config == null)
            return null;
        // ===== 修复:添加 Audio/ 前缀 =====
        AudioClip audioClip = BattleResManager.Instance.GetAudioResource(
            "Audio/" + config.Folder,  // ← 修复:添加 Audio/ 前缀
            config.Audio,
            battleField.guid
        );
        return audioClip;
        // ================================
    }
    
    /// <summary>