From 93cab33f292e99c81e738b2b6c58c7fa21a7f371 Mon Sep 17 00:00:00 2001
From: yyl <yyl>
Date: 星期三, 03 十二月 2025 17:42:37 +0800
Subject: [PATCH] 125 战斗 预加载资源Action

---
 Main/System/Battle/BattleResources/BattleCacheManager.cs.meta         |   11 
 Main/System/Battle/BattleResources/TeamResTracker.cs.meta             |   11 
 Main/System/Battle/BattleField/RecordActions/PreloadResAction.cs      |   61 ++
 Main/System/Battle/BattleObject/BattleObjectFactory.cs                |   24 
 Main/System/Battle/BattleResources/BattleUnloadManager.cs             |   91 +++
 Main/System/Battle/BattleResources/BattleResCache.cs                  |   93 +++
 Main/System/Battle/BattleResources/BattleResManager.cs                |   54 ++
 Main/System/Battle/RecordPlayer/RecordActionType.cs                   |    2 
 Main/System/Battle/BattleResources/TeamResTracker.cs                  |  192 +++++++
 Main/System/Battle/BattleField/BattleField.cs                         |   25 
 Main/System/Battle/BattleResources.meta                               |    8 
 Main/System/Battle/BattleResources/BattleResCache.cs.meta             |   11 
 Main/System/Battle/BattleResources/BattleResManager.cs.meta           |   11 
 Main/System/Battle/BattleResources/BattleCacheManager.cs              |  360 ++++++++++++++
 Main/System/Battle/BattleResources/BattleUnloadManager.cs.meta        |   11 
 Main/System/Battle/BattleResources/BattleAudioResLoader.cs.meta       |   11 
 Main/System/Battle/BattleResources/BattleAudioResLoader.cs            |  125 +++++
 Main/System/Battle/BattleResources/BattleSpineResLoader.cs.meta       |   11 
 Main/System/Battle/BattleResources/BattlePreloadManager.cs.meta       |   11 
 Main/System/Battle/BattleField/RecordActions/PreloadResAction.cs.meta |   11 
 Main/System/Battle/Sound/BattleSoundManager.cs                        |   17 
 Main/Component/UI/Effect/BattleEffectPlayer.cs                        |   18 
 Main/System/Battle/BattleResources/BattleSpineResLoader.cs            |  125 +++++
 Main/System/Battle/BattleResources/BattlePreloadManager.cs            |  188 +++++++
 24 files changed, 1,465 insertions(+), 17 deletions(-)

diff --git a/Main/Component/UI/Effect/BattleEffectPlayer.cs b/Main/Component/UI/Effect/BattleEffectPlayer.cs
index 8539e27..139fc2a 100644
--- a/Main/Component/UI/Effect/BattleEffectPlayer.cs
+++ b/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;
diff --git a/Main/System/Battle/BattleField/BattleField.cs b/Main/System/Battle/BattleField/BattleField.cs
index fdd0225..a56fe95 100644
--- a/Main/System/Battle/BattleField/BattleField.cs
+++ b/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;
+        }
+
+        // 浼犻�掓垬鍦篏UID
+        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浠h〃鐜╁澶辫触浜�
 
@@ -641,6 +661,9 @@
         
         // 娓呯悊姝讳骸澶勭悊璁板綍
         processingDeathObjIds.Clear();
+
+        // ===== 鏂板锛氬嵏杞借摑闃熻祫婧� =====
+        BattleResManager.Instance.UnloadBattleResources(guid);
     }
 
     //娓呭満鏁屾柟浣嗕笉缁堟鎴樻枟锛岀敤浜庡垏鎹富绾緽OSS鎴樻枟鍚庯紝姝e父鏄剧ず鏁屾柟
diff --git a/Main/System/Battle/BattleField/RecordActions/PreloadResAction.cs b/Main/System/Battle/BattleField/RecordActions/PreloadResAction.cs
new file mode 100644
index 0000000..278d4e3
--- /dev/null
+++ b/Main/System/Battle/BattleField/RecordActions/PreloadResAction.cs
@@ -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;
+		}
+
+		// 浼犻�掓垬鍦篏UID
+		BattleResManager.Instance.PreloadBattleResources(
+			battleField.guid,  // 鈫� 鍏抽敭锛氫紶閫掓垬鍦篏UID
+			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()
+	{
+		//姝e父寮�濮嬩箣鍚庡埌鐣岄潰鍑虹幇涔嬪墠閮界偣涓嶄簡 鎵�浠ヨ繖杈逛笉鐢ㄥ己鍒跺畬鎴� 鎺ュ彛鐣欑潃
+
+		base.ForceFinish();
+		// 瀹屾垚灏卞紑濮嬫樉绀篣I
+
+	}
+}
\ No newline at end of file
diff --git a/Main/System/Battle/BattleField/RecordActions/PreloadResAction.cs.meta b/Main/System/Battle/BattleField/RecordActions/PreloadResAction.cs.meta
new file mode 100644
index 0000000..5d30a06
--- /dev/null
+++ b/Main/System/Battle/BattleField/RecordActions/PreloadResAction.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: e694db14c5e25c6488fb04c8847759ae
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Main/System/Battle/BattleObject/BattleObjectFactory.cs b/Main/System/Battle/BattleObject/BattleObjectFactory.cs
index 82bbde4..58cae7d 100644
--- a/Main/System/Battle/BattleObject/BattleObjectFactory.cs
+++ b/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;
 
diff --git a/Main/System/Battle/BattleResources.meta b/Main/System/Battle/BattleResources.meta
new file mode 100644
index 0000000..d8dd4b3
--- /dev/null
+++ b/Main/System/Battle/BattleResources.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: eb0c5385e2bb4aa4b8860b129609a2c4
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Main/System/Battle/BattleResources/BattleAudioResLoader.cs b/Main/System/Battle/BattleResources/BattleAudioResLoader.cs
new file mode 100644
index 0000000..194214f
--- /dev/null
+++ b/Main/System/Battle/BattleResources/BattleAudioResLoader.cs
@@ -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();
+        }
+    }
+}
\ No newline at end of file
diff --git a/Main/System/Battle/BattleResources/BattleAudioResLoader.cs.meta b/Main/System/Battle/BattleResources/BattleAudioResLoader.cs.meta
new file mode 100644
index 0000000..e15c6fd
--- /dev/null
+++ b/Main/System/Battle/BattleResources/BattleAudioResLoader.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: ee1b6f0f511d6814b9d5d8c88ebbf62d
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Main/System/Battle/BattleResources/BattleCacheManager.cs b/Main/System/Battle/BattleResources/BattleCacheManager.cs
new file mode 100644
index 0000000..04230a8
--- /dev/null
+++ b/Main/System/Battle/BattleResources/BattleCacheManager.cs
@@ -0,0 +1,360 @@
+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>>();
+    
+    /// <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}";
+    }
+}
\ No newline at end of file
diff --git a/Main/System/Battle/BattleResources/BattleCacheManager.cs.meta b/Main/System/Battle/BattleResources/BattleCacheManager.cs.meta
new file mode 100644
index 0000000..4a5044f
--- /dev/null
+++ b/Main/System/Battle/BattleResources/BattleCacheManager.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 257fb2e8a69b6a44899a0111603f9fdc
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Main/System/Battle/BattleResources/BattlePreloadManager.cs b/Main/System/Battle/BattleResources/BattlePreloadManager.cs
new file mode 100644
index 0000000..f6885cd
--- /dev/null
+++ b/Main/System/Battle/BattleResources/BattlePreloadManager.cs
@@ -0,0 +1,188 @@
+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);
+        
+        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   // 鈫� 涓嶆槸绾㈤槦
+        );
+    }
+    
+    /// <summary>
+    /// 澶勭悊绾㈤槦鍙樻洿锛氭竻绌烘棫鐨勶紝閲嶆柊鍔犺浇鏂扮殑
+    /// </summary>
+    public void HandleRedTeamChange(List<TeamBase> newRedTeamList, Action completeCallback)
+    {
+        if (newRedTeamList == null)
+        {
+            completeCallback?.Invoke();
+            return;
+        }
+        
+        Debug.Log("BattlePreloadManager: Handling red team change");
+        
+        // 1. 鍗歌浇鏃х殑绾㈤槦璧勬簮
+        unloadManager.UnloadRedTeamResources(cacheManager);
+        
+        // 2. 鍒嗘瀽鏂扮孩闃熻祫婧�
+        var newRedInfo = AnalyzeTeamList(newRedTeamList, true);
+        
+        // 3. 棰勫姞杞芥柊绾㈤槦璧勬簮锛堢孩闃熸槸鍏ㄥ眬鐨勶紝浼犵┖瀛楃涓诧級
+        StartPreload(newRedInfo, new TeamResTracker.TeamResourceInfo(), "", null, completeCallback);
+    }
+}
\ No newline at end of file
diff --git a/Main/System/Battle/BattleResources/BattlePreloadManager.cs.meta b/Main/System/Battle/BattleResources/BattlePreloadManager.cs.meta
new file mode 100644
index 0000000..822374b
--- /dev/null
+++ b/Main/System/Battle/BattleResources/BattlePreloadManager.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: e34093150a553f343ac1758dd429544a
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Main/System/Battle/BattleResources/BattleResCache.cs b/Main/System/Battle/BattleResources/BattleResCache.cs
new file mode 100644
index 0000000..736befd
--- /dev/null
+++ b/Main/System/Battle/BattleResources/BattleResCache.cs
@@ -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;        // 璧勬簮鎵�鏈夎�匢D (鏍煎紡: 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;
+        }
+    }
+}
\ No newline at end of file
diff --git a/Main/System/Battle/BattleResources/BattleResCache.cs.meta b/Main/System/Battle/BattleResources/BattleResCache.cs.meta
new file mode 100644
index 0000000..a17e927
--- /dev/null
+++ b/Main/System/Battle/BattleResources/BattleResCache.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 57597e8314173bd498166dc244e55a96
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Main/System/Battle/BattleResources/BattleResManager.cs b/Main/System/Battle/BattleResources/BattleResManager.cs
new file mode 100644
index 0000000..4858268
--- /dev/null
+++ b/Main/System/Battle/BattleResources/BattleResManager.cs
@@ -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);
+    }
+}
\ No newline at end of file
diff --git a/Main/System/Battle/BattleResources/BattleResManager.cs.meta b/Main/System/Battle/BattleResources/BattleResManager.cs.meta
new file mode 100644
index 0000000..729ba5e
--- /dev/null
+++ b/Main/System/Battle/BattleResources/BattleResManager.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: b625f831b0c2aa0439ba1614b4159616
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Main/System/Battle/BattleResources/BattleSpineResLoader.cs b/Main/System/Battle/BattleResources/BattleSpineResLoader.cs
new file mode 100644
index 0000000..174d944
--- /dev/null
+++ b/Main/System/Battle/BattleResources/BattleSpineResLoader.cs
@@ -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();
+        }
+    }
+}
\ No newline at end of file
diff --git a/Main/System/Battle/BattleResources/BattleSpineResLoader.cs.meta b/Main/System/Battle/BattleResources/BattleSpineResLoader.cs.meta
new file mode 100644
index 0000000..1e0102b
--- /dev/null
+++ b/Main/System/Battle/BattleResources/BattleSpineResLoader.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: b7c61553574c7a045871fc98457bfe1a
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Main/System/Battle/BattleResources/BattleUnloadManager.cs b/Main/System/Battle/BattleResources/BattleUnloadManager.cs
new file mode 100644
index 0000000..1f51951
--- /dev/null
+++ b/Main/System/Battle/BattleResources/BattleUnloadManager.cs
@@ -0,0 +1,91 @@
+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");
+    }
+}
\ No newline at end of file
diff --git a/Main/System/Battle/BattleResources/BattleUnloadManager.cs.meta b/Main/System/Battle/BattleResources/BattleUnloadManager.cs.meta
new file mode 100644
index 0000000..0274a94
--- /dev/null
+++ b/Main/System/Battle/BattleResources/BattleUnloadManager.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: f103af01d502fa142b50f08dc14cdb67
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Main/System/Battle/BattleResources/TeamResTracker.cs b/Main/System/Battle/BattleResources/TeamResTracker.cs
new file mode 100644
index 0000000..56e20fa
--- /dev/null
+++ b/Main/System/Battle/BattleResources/TeamResTracker.cs
@@ -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);
+            
+            // 鍙鍔犺浇鎶�鑳界浉鍏宠祫婧�(闊虫晥銆佺壒鏁圫pine)
+            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璧勬簮锛堝彧棰勫姞杞界壒鏁圫pine锛屼笉棰勫姞杞借鑹睸pine锛�
+            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  // 鈫� 娣诲姞鎵�鏈夎�匢D
+                };
+                
+                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;
+    }
+}
\ No newline at end of file
diff --git a/Main/System/Battle/BattleResources/TeamResTracker.cs.meta b/Main/System/Battle/BattleResources/TeamResTracker.cs.meta
new file mode 100644
index 0000000..cd2a5b0
--- /dev/null
+++ b/Main/System/Battle/BattleResources/TeamResTracker.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 1ab0650d593061a4aaa2e7ad15d86d10
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Main/System/Battle/RecordPlayer/RecordActionType.cs b/Main/System/Battle/RecordPlayer/RecordActionType.cs
index cd8f4b3..fbc5588 100644
--- a/Main/System/Battle/RecordPlayer/RecordActionType.cs
+++ b/Main/System/Battle/RecordPlayer/RecordActionType.cs
@@ -12,4 +12,6 @@
     DodgeFinish,//闂伩瀹屾垚
 
     RoundChange,//鍥炲悎鍒囨崲
+
+    PreloadRes,//棰勫姞杞借祫婧�
 }
diff --git a/Main/System/Battle/Sound/BattleSoundManager.cs b/Main/System/Battle/Sound/BattleSoundManager.cs
index 7653235..de14247 100644
--- a/Main/System/Battle/Sound/BattleSoundManager.cs
+++ b/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>

--
Gitblit v1.8.0