yyl
2026-04-03 99a11d2bb19d74f6cc8584ac16838062af4fb301
webgl 优化
39个文件已修改
2个文件已添加
635 ■■■■ 已修改文件
Main/Component/UI/Common/ButtonClickInterval.cs 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Component/UI/Decorate/Tweens/FillTween.cs 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Component/UI/Decorate/Tweens/TweenEx.cs 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Component/UI/Decorate/Tweens/UIAlphaTween.cs 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Component/UI/Effect/EllipseMask.cs 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Component/UI/Effect/TimeMgr.cs 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Core/GameEngine/Launch/YooAssetInitTask.cs 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Core/NetworkPackage/Socket/ClientSocket.cs 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Manager/UIManager.cs 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/ResModule/YooAssetService.cs 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/SDK/SDKUtils.cs 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/BattleEffectMgr.cs 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/BattleField/RecordActions/SkillRecordAction.cs 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/BattleManager.cs 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/BattleObject/BattleObjMgr.cs 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/BattleObject/HeroBattleObject.cs 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/BattleUtility.cs 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/Buff/BattleObjectBuffMgr.cs 39 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/Motion/MotionBase.cs 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/RecordPlayer/RecordAction.cs 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/RecordPlayer/RecordPlayer.cs 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/Skill/SkillBase.cs 86 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/SkillEffect/BulletSkillEffect.cs 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/SkillEffect/DotSkillEffect.cs 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/SkillEffect/NoEffect.cs 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/SkillEffect/NormalSkillEffect.cs 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/SkillEffect/SkillEffect.cs 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/Sound/BattleSoundManager.cs 53 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Equip/EquipTipWin.cs 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Hero/UIHeroController.cs 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Main/MainWin.cs 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/PhantasmPavilion/AvatarCell.cs 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Sound/SoundPlayer.cs 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/SpineUpdateManager.cs 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Tip/MarqueeWin.cs 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Tip/ScrollTip.cs 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Utility/FrameEffect.cs 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Utility/UGUIEventListenerContainDrag.cs 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Utility/UIHelper.cs 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Utility/WaitForSecondsCache.cs 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Utility/WaitForSecondsCache.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Component/UI/Common/ButtonClickInterval.cs
@@ -22,7 +22,7 @@
    IEnumerator DelayClick()
    {
        yield return new WaitForSeconds(interval);
        yield return WaitForSecondsCache.Get(interval);
        targetBtn.enabled = true;
    }
}
Main/Component/UI/Decorate/Tweens/FillTween.cs
@@ -52,7 +52,7 @@
        if (this.gameObject.activeInHierarchy)
        {
            SetStartState();
            StartCoroutine("Co_StartTween");
            StartCoroutine(Co_StartTween());
        }
    }
@@ -72,7 +72,7 @@
                SetStartState();
            }
            StartCoroutine("Co_StartTween");
            StartCoroutine(Co_StartTween());
        }
    }
@@ -84,7 +84,7 @@
        if (this.gameObject.activeInHierarchy)
        {
            SetStartState();
            StartCoroutine("Co_StartTween");
            StartCoroutine(Co_StartTween());
        }
    }
@@ -102,7 +102,7 @@
        if (trigger == Trigger.Start)
        {
            SetStartState();
            StartCoroutine("Co_StartTween");
            StartCoroutine(Co_StartTween());
        }
    }
@@ -111,7 +111,7 @@
        if (trigger == Trigger.Enable)
        {
            SetStartState();
            StartCoroutine("Co_StartTween");
            StartCoroutine(Co_StartTween());
        }
    }
@@ -152,7 +152,7 @@
        doTween = false;
        OnPrepare();
        yield return new WaitForSeconds(delay);
        yield return WaitForSecondsCache.Get(delay);
        curveLength = curve.keys[curve.keys.Length - 1].time - curve.keys[0].time;
        doTween = true;
        accumulatedTime = 0f;
Main/Component/UI/Decorate/Tweens/TweenEx.cs
@@ -144,7 +144,7 @@
            case DelayMode.Time:
                if (delay > 0.001f)
                {
                    yield return new WaitForSeconds(delay);
                    yield return WaitForSecondsCache.Get(delay);
                }
                break;
        }
Main/Component/UI/Decorate/Tweens/UIAlphaTween.cs
@@ -53,7 +53,7 @@
        if (this.gameObject.activeInHierarchy)
        {
            SetStartState();
            StartCoroutine("Co_StartTween");
            StartCoroutine(Co_StartTween());
        }
    }
@@ -73,7 +73,7 @@
                SetStartState();
            }
            StartCoroutine("Co_StartTween");
            StartCoroutine(Co_StartTween());
        }
    }
@@ -85,7 +85,7 @@
        if (this.gameObject.activeInHierarchy)
        {
            SetStartState();
            StartCoroutine("Co_StartTween");
            StartCoroutine(Co_StartTween());
        }
    }
@@ -103,7 +103,7 @@
        if (trigger == Trigger.Start)
        {
            SetStartState();
            StartCoroutine("Co_StartTween");
            StartCoroutine(Co_StartTween());
        }
    }
@@ -112,7 +112,7 @@
        if (trigger == Trigger.Enable)
        {
            SetStartState();
            StartCoroutine("Co_StartTween");
            StartCoroutine(Co_StartTween());
        }
    }
@@ -153,7 +153,7 @@
        doTween = false;
        OnPrepare();
        yield return new WaitForSeconds(delay);
        yield return WaitForSecondsCache.Get(delay);
        curveLength = curve.keys[curve.keys.Length - 1].time - curve.keys[0].time;
        doTween = true;
        accumulatedTime = 0f;
Main/Component/UI/Effect/EllipseMask.cs
@@ -23,6 +23,8 @@
    private Image m_MaskImage;
    private RectTransform m_RectTransform;
    private List<Graphic> m_MaskedChildren = new List<Graphic>();
    // 缓存GetComponentsInChildren结果,避免每次OnEnable分配数组
    private static List<Graphic> _graphicCacheList = new List<Graphic>();
    public Vector2 EllipseCenter
    {
@@ -228,10 +230,12 @@
        // 清除之前的列表
        m_MaskedChildren.Clear();
        
        // 获取所有子对象的Graphic组件
        Graphic[] childrenGraphics = GetComponentsInChildren<Graphic>();
        foreach (var graphic in childrenGraphics)
        // 使用静态缓存列表避免每次分配数组
        _graphicCacheList.Clear();
        GetComponentsInChildren(false, _graphicCacheList);
        for (int i = 0; i < _graphicCacheList.Count; i++)
        {
            var graphic = _graphicCacheList[i];
            // 跳过遮罩本身
            if (graphic.gameObject == this.gameObject)
                continue;
@@ -240,6 +244,7 @@
            CreateChildMaskMaterial(graphic);
            m_MaskedChildren.Add(graphic);
        }
        _graphicCacheList.Clear();
    }
    /// <summary>
Main/Component/UI/Effect/TimeMgr.cs
@@ -59,7 +59,7 @@
        {
            Debug.Log(e.StackTrace);
        }
        for (int i = 0; i < syntonyList.Count; i++)
        for (int i = syntonyList.Count - 1; i >= 0; i--)
        {
            if ((TimeUtility.ServerNow - syntonyList[i].endTime).TotalSeconds > 0)
            {
@@ -68,21 +68,24 @@
                    syntonyList[i].callback();
                }
                var _type = syntonyList[i].type;
                syntonyList.RemoveAt(i);
                // Swap-remove避免O(n)移位
                int last = syntonyList.Count - 1;
                if (i < last)
                    syntonyList[i] = syntonyList[last];
                syntonyList.RemoveAt(last);
                if (OnSyntonyEvent != null)
                {
                    OnSyntonyEvent(_type);
                }
                i--;
            }
        }
        if (timeItems.Count > 0)
        {
            timeItemList.RemoveRange(0, timeItemList.Count);
            foreach (Component item in timeItems.Keys)
            timeItemList.Clear();
            foreach (var kv in timeItems)
            {
                if (item == null) continue;
                timeItemList.Add(timeItems[item]);
                if (kv.Key == null) continue;
                timeItemList.Add(kv.Value);
            }
            for (int i = 0; i < timeItemList.Count; i++)
            {
Main/Core/GameEngine/Launch/YooAssetInitTask.cs
@@ -38,6 +38,7 @@
        {
            // Determine play mode based on AssetSource setting
            EPlayMode playMode;
            YooAsset.IRemoteServices remoteServices = null;
            if (!AssetSource.isUseAssetBundle)
            {
                // Editor 不使用 AB 模式:EditorSimulateMode 直接读 AssetDatabase
@@ -48,6 +49,8 @@
#if UNITY_WEBGL
                // WebGL 平台(含 Editor 下切到 WebGL target):BuildInFileSystem 不支持 WebGL,必须用 WebPlayMode
                playMode = EPlayMode.WebPlayMode;
                // 远程模式:资源不随包,从 HTTP 服务器加载
                remoteServices = WebGLRemoteConfig.CreateRemoteServices();
#elif UNITY_EDITOR
                // Editor 非 WebGL target + AB 模式:从本地 StreamingAssets 加载已构建的 AB
                playMode = EPlayMode.OfflinePlayMode;
@@ -58,7 +61,7 @@
            }
            // Initialize YooAssetService
            await YooAssetService.Instance.InitializeAsync(playMode);
            await YooAssetService.Instance.InitializeAsync(playMode, remoteServices);
            // Register as IYooAssetBridge for Launch assembly cross-assembly access
            YooAssetBridgeHolder.Register(YooAssetService.Instance);
Main/Core/NetworkPackage/Socket/ClientSocket.cs
@@ -32,6 +32,7 @@
    // WebSocket 实现(WebGL平台)
    WebSocket webSocket;
    public WebSocket socket { get { return webSocket; } }
    private byte[] fragmentBytes; // TCP-to-WS网关按TCP缓冲区拆包,需要跨消息重组
#endif
    public Action OnDisconnected;
@@ -489,8 +490,16 @@
        {
            getBytesTotal += data.Length;
            
            // WebSocket是消息模式,每次收到完整包,直接处理
            byte[] fixBytes = data;
            // TCP-to-WS网关按TCP缓冲区大小拆分,需跨消息重组(与TCP ReadInfo逻辑一致)
            if (fragmentBytes != null && fragmentBytes.Length > 0)
            {
                fixBytes = new byte[fragmentBytes.Length + data.Length];
                Array.Copy(fragmentBytes, 0, fixBytes, 0, fragmentBytes.Length);
                Array.Copy(data, 0, fixBytes, fragmentBytes.Length, data.Length);
            }
            fragmentBytes = null;
            int vReadIndex = 0;
            byte[] vPackBytes;
            int vLeavingLeng = 0;
@@ -503,14 +512,30 @@
                vLeavingLeng = vTotalLeng - vReadIndex;
                if (vLeavingLeng < 6)
                {
                    Debug.LogError($"[ClientSocket-WebSocket] 包数据不足: {vLeavingLeng} bytes");
                    fragmentBytes = new byte[vLeavingLeng];
                    Array.Copy(fixBytes, vReadIndex, fragmentBytes, 0, vLeavingLeng);
                    break;
                }
                // 校验FFCC包头,防止数据错位
                if (fixBytes[vReadIndex] != 0xFF || fixBytes[vReadIndex + 1] != 0xCC)
                {
                    Debug.LogError($"[ClientSocket-WebSocket] FFCC包头异常: {fixBytes[vReadIndex]:X2} {fixBytes[vReadIndex + 1]:X2}, 丢弃剩余 {vLeavingLeng} 字节");
                    fragmentBytes = null;
                    break;
                }
                
                vBodyLeng = BitConverter.ToInt32(fixBytes, vReadIndex + 2);
                if (vBodyLeng <= 0)
                {
                    Debug.LogError($"[ClientSocket-WebSocket] 包体长度非法: {vBodyLeng}, 丢弃");
                    fragmentBytes = null;
                    break;
                }
                if (vBodyLeng > vLeavingLeng - 6)
                {
                    Debug.LogError($"[ClientSocket-WebSocket] 包长度不匹配: 声明 {vBodyLeng + 6}, 实际 {vLeavingLeng}");
                    fragmentBytes = new byte[vLeavingLeng];
                    Array.Copy(fixBytes, vReadIndex, fragmentBytes, 0, vLeavingLeng);
                    break;
                }
                
@@ -573,6 +598,7 @@
    public async void CloseConnect()
    {
        Debug.Log("[ClientSocket-WebSocket] ==== CloseConnect");
        fragmentBytes = null;
        
        if (webSocket != null)
        {
Main/Manager/UIManager.cs
@@ -48,6 +48,10 @@
    // 当前最高的排序顺序
    private int currentHighestSortingOrder = 0;
    
    // 排序缓存,避免每次UpdateUISortingOrder分配
    private List<UIBase> _sortTempList = new List<UIBase>(32);
    private Dictionary<UIBase, int> _sortOrderDict = new Dictionary<UIBase, int>(32);
    // 当前回合数,用于记录UI的使用情况
    private int currentRound = 0;
    
@@ -923,34 +927,35 @@
        // 重置当前最高排序顺序
        currentHighestSortingOrder = 0;
        
        // 遍历UI栈,设置排序顺序
        UIBase[] uiArray = new UIBase[uiStack.Count];
        uiStack.CopyTo(uiArray, 0);
        // WebGL/IL2CPP 下被 Destroy 的对象访问任何属性都会 NullReferenceException,
        // 必须在使用前过滤掉(Unity 伪 null:C# 引用非 null 但 == null 为 true)
        uiArray = System.Array.FindAll(uiArray, ui => ui != null);
        // 复用临时列表,避免每次分配数组
        _sortTempList.Clear();
        _sortOrderDict.Clear();
        
        // 先按照UILayer进行排序,然后再按照栈顺序排序
        Dictionary<UIBase, int> uiOrderDict = new Dictionary<UIBase, int>();
        for (int i = 0; i < uiArray.Length; i++)
        int index = 0;
        foreach (var ui in uiStack)
        {
            uiOrderDict[uiArray[i]] = i;
            if (ui != null)
            {
                _sortTempList.Add(ui);
                _sortOrderDict[ui] = index;
                index++;
            }
        }
        Array.Sort(uiArray, (a, b) =>
        _sortTempList.Sort((a, b) =>
        {
            if (a == null || b == null) return 0;
            int layerCompare = a.uiLayer.CompareTo(b.uiLayer);
            if (layerCompare != 0)
                return layerCompare;
            return uiOrderDict[b].CompareTo(uiOrderDict[a]);
            return _sortOrderDict[b].CompareTo(_sortOrderDict[a]);
        });
        // 遍历排序后的UI数组,设置排序顺序
        foreach (var ui in uiArray)
        // 遍历排序后的UI列表,设置排序顺序
        for (int i = 0; i < _sortTempList.Count; i++)
        {
            var ui = _sortTempList[i];
            if (ui == null) continue;
            // 获取基础排序顺序
            int baseSortingOrder = GetBaseSortingOrderForLayer(ui.uiLayer);
Main/ResModule/YooAssetService.cs
@@ -266,13 +266,19 @@
                    webParams.WebServerFileSystemParameters = TiktokFileSystemCreater
                        .CreateFileSystemParameters(packageRoot, remoteServices);
#else
                    webParams.WebServerFileSystemParameters = FileSystemParameters
                        .CreateDefaultWebServerFileSystemParameters();
                    if (remoteServices != null)
                    {
                        webParams.WebRemoteFileSystemParameters = FileSystemParameters
                        // 远程模式(LocalCDN/RemoteCDN):资源不在 StreamingAssets,
                        // 跳过 WebServerFileSystem,只用 WebRemoteFileSystem 从 HTTP 服务器加载
                        webParams.WebServerFileSystemParameters = FileSystemParameters
                            .CreateDefaultWebRemoteFileSystemParameters(remoteServices);
                    }
                    else
                    {
                        // Local 模式:资源在 StreamingAssets,用 WebServerFileSystem
                        webParams.WebServerFileSystemParameters = FileSystemParameters
                            .CreateDefaultWebServerFileSystemParameters();
                    }
#endif
                    return webParams;
                }
Main/SDK/SDKUtils.cs
@@ -239,7 +239,7 @@
                }
            }
            yield return new WaitForSeconds(1f);
            yield return WaitForSecondsCache.Wait1;
            // yield return WaitingForSecondConst.WaitMS1000;
        }
    }
Main/System/Battle/BattleEffectMgr.cs
@@ -10,6 +10,9 @@
    private Dictionary<int, List<BattleEffectPlayer>> effectDict = new Dictionary<int, List<BattleEffectPlayer>>();
    // 缓存Run()的临时列表,避免每帧分配
    private List<BattleEffectPlayer> _runList = new List<BattleEffectPlayer>();
    public void Init(BattleField _battleField)
    {
        Reload(_battleField);
@@ -23,16 +26,16 @@
    public void Run()
    {
        List<BattleEffectPlayer> runList = new List<BattleEffectPlayer>();
        _runList.Clear();
        foreach (KeyValuePair<int, List<BattleEffectPlayer>> kvPair in effectDict)
        {
            runList.AddRange(kvPair.Value);
            _runList.AddRange(kvPair.Value);
        }
        for (int i = runList.Count - 1; i >= 0; i--)
        for (int i = _runList.Count - 1; i >= 0; i--)
        {
            BattleEffectPlayer effectPlayer = runList[i];
            BattleEffectPlayer effectPlayer = _runList[i];
            if (effectPlayer != null)
            {
                effectPlayer.Run();
@@ -169,11 +172,13 @@
    public void Release()
    {
        List<int> fKeys = effectDict.Keys.ToList();
        for (int i = 0; i < fKeys.Count; i++)
        // 必须先快照key列表,因为DestroyImmediate会同步触发OnEffectDestroy修改effectDict
        _runList.Clear();
        var keys = new List<int>(effectDict.Keys);
        for (int k = 0; k < keys.Count; k++)
        {
            List<BattleEffectPlayer> effectPlayers = effectDict[fKeys[i]];
            if (!effectDict.TryGetValue(keys[k], out var effectPlayers))
                continue;
            while (effectPlayers.Count > 0)
            {
                var effectPlayer = effectPlayers[0];
Main/System/Battle/BattleField/RecordActions/SkillRecordAction.cs
@@ -13,11 +13,11 @@
public class SkillRecordAction : RecordAction
{
#if UNITY_EDITOR
// #if UNITY_EDITOR
    public 
#else
    protected
#endif
// #else
//     protected
// #endif
        SkillBase skillBase;
Main/System/Battle/BattleManager.cs
@@ -12,6 +12,8 @@
    //  同时只能有一场战斗在进行 guid, battlefield
    protected Dictionary<string, BattleField> battleFields = new Dictionary<string, BattleField>();
    // 缓存key列表,避免Run()每帧分配
    private List<string> _runKeysCache = new List<string>();
    public float[] speedGear; //战斗倍数对应的实际速率
    public int speedIndex
@@ -631,10 +633,13 @@
    public void Run()
    {
        List<string> keys = new List<string>(battleFields.Keys);
        for (int i = keys.Count - 1; i >= 0; i--)
        _runKeysCache.Clear();
        foreach (var key in battleFields.Keys)
            _runKeysCache.Add(key);
        for (int i = _runKeysCache.Count - 1; i >= 0; i--)
        {
            var battleField = battleFields[keys[i]];
            if (!battleFields.TryGetValue(_runKeysCache[i], out var battleField))
                continue;
            try
            {
                battleField?.Run();
Main/System/Battle/BattleObject/BattleObjMgr.cs
@@ -8,8 +8,39 @@
public class BattleObjMgr
{
    //  死亡不可以将BattleObject移出字典/列表
    public List<BattleObject> redCampList => new List<BattleObject>(redCampDict.Values);
    public List<BattleObject> blueCampList => new List<BattleObject>(blueCampDict.Values);
    // 缓存阵营列表,仅在字典变更时重建,避免每次属性访问分配新List
    private List<BattleObject> _redCampListCache;
    private List<BattleObject> _blueCampListCache;
    private bool _redCampDirty = true;
    private bool _blueCampDirty = true;
    public List<BattleObject> redCampList
    {
        get
        {
            if (_redCampDirty || _redCampListCache == null)
            {
                if (_redCampListCache == null) _redCampListCache = new List<BattleObject>(redCampDict.Count);
                else _redCampListCache.Clear();
                foreach (var kv in redCampDict) _redCampListCache.Add(kv.Value);
                _redCampDirty = false;
            }
            return _redCampListCache;
        }
    }
    public List<BattleObject> blueCampList
    {
        get
        {
            if (_blueCampDirty || _blueCampListCache == null)
            {
                if (_blueCampListCache == null) _blueCampListCache = new List<BattleObject>(blueCampDict.Count);
                else _blueCampListCache.Clear();
                foreach (var kv in blueCampDict) _blueCampListCache.Add(kv.Value);
                _blueCampDirty = false;
            }
            return _blueCampListCache;
        }
    }
    private Dictionary<int, BattleObject> redCampDict = new Dictionary<int, BattleObject>();
    private Dictionary<int, BattleObject> blueCampDict = new Dictionary<int, BattleObject>();
@@ -40,6 +71,12 @@
        await CreateTeam(posNodeList, campDict, teamBase, _camp, active);
    }
    private void MarkCampDirty(Dictionary<int, BattleObject> campDict)
    {
        if (campDict == redCampDict) _redCampDirty = true;
        else if (campDict == blueCampDict) _blueCampDirty = true;
    }
    protected async UniTask CreateTeam(List<GameObject> posNodeList, Dictionary<int, BattleObject> campDict, TeamBase teamBase, BattleCamp _Camp, bool active)
    {
        DestroyTeam(campDict);
@@ -61,6 +98,7 @@
                battleObj.SetSpeedRatio(battleField.speedRatio);
            }
        }
        MarkCampDirty(campDict);
        if (teamBase.teamMingge != null)
        {
@@ -163,10 +201,12 @@
                if (battleObj.Camp == BattleCamp.Red)
                {
                    redCampDict.Remove(battleObj.GetPositionNum());
                    _redCampDirty = true;
                }
                else
                {
                    blueCampDict.Remove(battleObj.GetPositionNum());
                    _blueCampDirty = true;
                }
                allBattleObjDict.Remove((int)objID);
                BattleObjectFactory.DestroyBattleObject((int)objID, battleObj);
@@ -187,6 +227,7 @@
            }
        }
        campDict.Clear();
        MarkCampDirty(campDict);
    }
    //  空闲状态
Main/System/Battle/BattleObject/HeroBattleObject.cs
@@ -201,6 +201,15 @@
    {
        return motionBase.CanCastSkill(skillSkinConfig);
    }
    /// <summary>
    /// 强制重置MotionBase的技能动画状态
    /// 用于技能ForceFinish后防止playingSkillWithAnim卡住后续技能
    /// </summary>
    public void ForceResetMotionSkillState()
    {
        motionBase?.ForceResetSkillState();
    }
    
    public override SkeletonAnimation GetSkeletonAnimation()
    {
Main/System/Battle/BattleUtility.cs
@@ -161,6 +161,11 @@
    public static int GetMainTargetPositionNum(SkillBase skillBase, BattleObject caster, List<HB427_tagSCUseSkill.tagSCUseSkillHurt> targetList, SkillConfig skillConfig)
    {
        return GetMainTargetPositionNum(skillBase, caster, (IReadOnlyList<HB427_tagSCUseSkill.tagSCUseSkillHurt>)targetList, skillConfig);
    }
    public static int GetMainTargetPositionNum(SkillBase skillBase, BattleObject caster, IReadOnlyList<HB427_tagSCUseSkill.tagSCUseSkillHurt> targetList, SkillConfig skillConfig)
    {
        int returnIndex = 0;
        //  根据敌方血量阵营 存活人数来选择
        BattleCamp battleCamp = skillConfig.TagFriendly != 0 ? caster.Camp : caster.GetEnemyCamp();
@@ -286,7 +291,7 @@
                var fromSkill = skillBase.fromSkill;
                if (fromSkill != null)
                {
                    returnIndex = GetMainTargetPositionNum(fromSkill, fromSkill.caster, fromSkill.tagUseSkillAttack.HurtList.ToList(), fromSkill.skillConfig);
                    returnIndex = GetMainTargetPositionNum(fromSkill, fromSkill.caster, fromSkill.tagUseSkillAttack.HurtList, fromSkill.skillConfig);
                }
                else
                {
Main/System/Battle/Buff/BattleObjectBuffMgr.cs
@@ -27,6 +27,12 @@
        { BattleConst.PassiveSkillLimitGroup, false },
    };
    // 缓存Run()清理用的列表,避免每帧分配
    private List<int> _removeEffectCache = new List<int>();
    // 缓存OnBuffChanged回调用的列表,避免每次分配
    private List<HB428_tagSCBuffRefresh> _buffValueCache = new List<HB428_tagSCBuffRefresh>();
    public void Init(BattleObject _battleObject)
    {
        battleObject = _battleObject;
@@ -48,7 +54,7 @@
    public void Run()
    {
        List<int> removeEffectList = new List<int>();
        _removeEffectCache.Clear();
        //  跟随BattleObject
        foreach (var kv in buffEffectDict)
        {
@@ -58,7 +64,7 @@
                if (effectPlayer.isBindBone)
                {
                    effectPlayer.FollowBoneXY();
                    return;
                    continue;
                }
                int[] effectPos = effectPlayer.effectConfig.effectPos;
                effectPlayer.transform.position = battleObject.GetPosition();
@@ -69,11 +75,11 @@
            }
            else
            {
                removeEffectList.Add(kv.Key);
                _removeEffectCache.Add(kv.Key);
            }
        }
        
        foreach (var effectId in removeEffectList)
        foreach (var effectId in _removeEffectCache)
        {
            buffEffectDict.Remove(effectId);
        }
@@ -234,8 +240,15 @@
            return;
        }
        var buffList = vNetDataList.Where(buff => buff != null && buff.IsAdd != 0).ToList();
        var refreshList = vNetDataList.Where(buff => buff != null && buff.IsAdd == 0).ToList();
        var buffList = new List<HB428_tagSCBuffRefresh>();
        var refreshList = new List<HB428_tagSCBuffRefresh>();
        for (int i = 0; i < vNetDataList.Count; i++)
        {
            var buff = vNetDataList[i];
            if (buff == null) continue;
            if (buff.IsAdd != 0) buffList.Add(buff);
            else refreshList.Add(buff);
        }
        // 处理需要播放动画的buff (IsAdd != 0)
        if (buffList.Count > 0)
@@ -345,7 +358,10 @@
        UpdateControlState();
        battleObject.RefreshBuff(buffDataDict.Values.ToList());
        _buffValueCache.Clear();
        foreach (var kv in buffDataDict)
            _buffValueCache.Add(kv.Value);
        battleObject.RefreshBuff(_buffValueCache);
        onBuffChanged?.Invoke();
        // bool isUnderControl = false;
@@ -415,11 +431,14 @@
    public List<HB428_tagSCBuffRefresh> GetBuffIconList()
    {
        List<HB428_tagSCBuffRefresh> buffList = buffDataDict.Values.Where(buff =>
        List<HB428_tagSCBuffRefresh> buffList = new List<HB428_tagSCBuffRefresh>();
        foreach (var kv in buffDataDict)
        {
            var buff = kv.Value;
            SkillConfig skillConfig = SkillConfig.Get((int)buff.SkillID);
            return skillConfig != null;
        }).ToList();
            if (skillConfig != null)
                buffList.Add(buff);
        }
        return buffList;
    }
Main/System/Battle/Motion/MotionBase.cs
@@ -757,4 +757,44 @@
    {
        return !playingSkillWithAnim;
    }
    /// <summary>
    /// 强制重置技能动画状态标记
    /// 用于技能ForceFinish后确保不会阻塞后续技能
    /// </summary>
    public void ForceResetSkillState()
    {
        // 清理所有技能轨道(保留死亡轨道9)
        List<int> tracksToRemove = new List<int>();
        foreach (var kv in activeSkillTracks)
        {
            if (kv.Key != DeathTrackIndex)
            {
                tracksToRemove.Add(kv.Key);
                if (animState != null)
                {
                    animState.ClearTrack(kv.Key);
                }
            }
        }
        foreach (int track in tracksToRemove)
        {
            activeSkillTracks.Remove(track);
        }
        // 回收所有子技能轨道
        foreach (var kv in subSkillTrackMap)
        {
            if (availableSubTracks != null)
                availableSubTracks.Enqueue(kv.Value);
        }
        subSkillTrackMap.Clear();
        // 重置标记
        if (!HasActiveSkillTracks())
        {
            playingSkill = false;
            playingSkillWithAnim = false;
        }
    }
}
Main/System/Battle/RecordPlayer/RecordAction.cs
@@ -41,8 +41,14 @@
    //  自身动作是否完成(不包括子节点的完成状态)
    protected bool isActionCompleted = false;
    //  ===== 卡死检测机制 =====
    //  累计运行时间(仅在Run()被调用时累加,暂停时不计时)
    private float accumulatedRunTime = 0f;
    //  最大允许运行时长(秒),超过则强制结束
    protected virtual float MaxActionDuration => 15f;
    //  ===== 内部RecordPlayer机制 =====
    //  内部RecordPlayer:用于播放由此RecordAction内部产生的RecordAction
    //  内部RecordPlayer:���于播放由此RecordAction内部产生的RecordAction
    //  当RecordAction内部产生新的RecordAction时(如PackageRegedit.Distribute触发的),
    //  这些RecordAction应该由当前RecordAction的innerRecordPlayer管理,而不是BattleField的主RecordPlayer
    //  这样可以保证:
@@ -177,8 +183,26 @@
        return true;
    }
    //  检测当前Action是否卡死(运行时间超过允许上限)
    //  注意:使用累计时间而非Time.time,这样暂停期间不会计时
    public bool IsStuck()
    {
        if (accumulatedRunTime <= 0f) return false;
        if (isFinish || isActionCompleted) return false;
        return accumulatedRunTime > MaxActionDuration;
    }
    //  获取已运行时长
    public float GetRunDuration()
    {
        return accumulatedRunTime;
    }
    public virtual void Run()
    {
        //  累加运行时间(暂停时BattleField.Run不调用RecordPlayer,所以此处不会累加)
        accumulatedRunTime += Time.deltaTime;
#if UNITY_EDITOR
        // 首次运行时打印调试信息
        if (!hasLoggedFirstRun)
Main/System/Battle/RecordPlayer/RecordPlayer.cs
@@ -259,6 +259,15 @@
                
                if (!action.IsFinished())
                {
                    //  卡死检测:如果immediately action运行时间过长,强制结束
                    if (action.IsStuck())
                    {
                        BattleDebug.LogError($"RecordPlayer: ImmediatelyAction {action.GetType().Name} (ID:{action.actionID}) 运行 {action.GetRunDuration():F1}s 超时,强制结束!");
                        action.ForceFinish();
                        removeIndexList.Add(i);
                        continue;
                    }
                    if (action.waitingAnimeAction != null)
                    {
                        if (!action.waitingAnimeAction.IsActionCompleted())
@@ -350,6 +359,15 @@
        if (currentRecordAction != null && !currentRecordAction.IsFinished())
        {
            //  卡死检测:如果currentRecordAction运行时间过长,强制结束
            if (currentRecordAction.IsStuck())
            {
                BattleDebug.LogError($"RecordPlayer: CurrentAction {currentRecordAction.GetType().Name} (ID:{currentRecordAction.actionID}) 运行 {currentRecordAction.GetRunDuration():F1}s 超时,强制结束!");
                currentRecordAction.ForceFinish();
                currentRecordAction = null;
                return;
            }
            if (currentRecordAction.waitingAnimeAction != null)
            {
                if (!currentRecordAction.waitingAnimeAction.IsActionCompleted())
Main/System/Battle/Skill/SkillBase.cs
@@ -195,7 +195,7 @@
        
    }
    // 技能运行主逻辑:处理技能效果和其他技能动作
    // 技能运行主逻辑:仅驱动技能效果(skillEffect),子技能和死亡由IsFinished()推进
    public virtual void Run()
    {
        if (skillEffect != null)
@@ -211,8 +211,6 @@
            }
            return;
        }
    }
    protected void ShadowIllutionCreate(bool create)
@@ -370,7 +368,7 @@
            return;
        }
        int mainTargetPosNum = BattleUtility.GetMainTargetPositionNum(this, caster, tagUseSkillAttack.HurtList.ToList(), skillConfig);
        int mainTargetPosNum = BattleUtility.GetMainTargetPositionNum(this, caster, tagUseSkillAttack.HurtList, skillConfig);
        BattleCamp battleCamp = skillConfig.TagFriendly != 0 ? caster.Camp : caster.GetEnemyCamp();
        RectTransform targetTrans = battleField.GetTeamNode(battleCamp, mainTargetPosNum);
@@ -482,9 +480,9 @@
    protected virtual void OnAllAttackMoveFinished()
    {
        moveFinished = true;
        List<BattleObject> allList = battleField.battleObjMgr.allBattleObjDict.Values.ToList<BattleObject>();
        foreach (BattleObject bo in allList)
        foreach (var kv in battleField.battleObjMgr.allBattleObjDict)
        {
            BattleObject bo = kv.Value;
            bo.layerMgr.SetFront();
            bo.GetHeroInfoBar()?.SetActive(true);
        }
@@ -672,7 +670,7 @@
        // 确保施法者也被高亮(原逻辑)
        var highlightList = new List<BattleObject>(targetSet) { caster };
        var allList = battleField.battleObjMgr.allBattleObjDict.Values.ToList();
        var allList = battleField.battleObjMgr.allBattleObjDict.Values;
        // 构造集合便于判断
        var targetSetLookup = new HashSet<BattleObject>(targetSet);
@@ -1022,7 +1020,12 @@
        // 获取并分配掉落物品和经验
        var dropPack = PackManager.Instance.GetSinglePack(PackType.DropItem);
        var itemDict = dropPack.GetAllItems();
        List<ItemModel> itemList = new List<ItemModel>(itemDict.Values.Where(item => item != null && item.isAuction));
        List<ItemModel> itemList = new List<ItemModel>();
        foreach (var item in itemDict.Values)
        {
            if (item != null && item.isAuction)
                itemList.Add(item);
        }
        var dropAssign = AssignDrops(itemList, deadPackList.Count);
        var expAssign = AssignExp(expPackList, deadPackList.Count);
@@ -1041,7 +1044,9 @@
                continue;
            }
            
            List<int> itemIndexList = dropAssign[i].Select(item => item.gridIndex).ToList();
            List<int> itemIndexList = new List<int>(dropAssign[i].Count);
            for (int j = 0; j < dropAssign[i].Count; j++)
                itemIndexList.Add(dropAssign[i][j].gridIndex);
            
            BattleDrops battleDrops = new BattleDrops()
            {
@@ -1238,7 +1243,8 @@
        return false;
    }
    // 检查技能是否完成:综合检查所有完成条件
    // 检查技能是否完成:同时推进状态(清理完成的子技能、处理剩余包、触发死亡判定)
    // 注意:此方法有副作用,这是设计使然——由SkillRecordAction.Run()每帧调用来驱动状态推进
    public virtual bool IsFinished()
    {
        if (!isPlay) return false;
@@ -1254,7 +1260,7 @@
            tempRetValue = false;
        }
        // 检查其他技能动作是否完成
        // 检查跟进的技能动作是否完成(追击/连击/反击等)
        if (currentWaitingSkill.Count > 0)
        {
            if (currentWaitingSkill.Any(s => s.IsFinished()))
@@ -1273,7 +1279,6 @@
            return false;
        }
        // 检查最终完成状态
        if (isFinished && moveFinished)
        {
@@ -1283,7 +1288,7 @@
                return false;
            }
            //  如果自己内部的recora action的 inner record player还有没执行完的包 也是返回false
            //  如果自己内部的 innerRecordPlayer 还有没执行完的包 也是返回false
            if (ownRecordAction != null && ownRecordAction.GetInnerRecordPlayer().IsPlaying())
            {
                return false;
@@ -1294,7 +1299,6 @@
            {
                battleField.RemoveCastingSkill(caster.ObjID, this);
                
                //  传递parentRecordAction,让死亡技能等待当前技能完成
                DeathRecordAction recordAction = battleField.OnObjsDead(new List<BattleDeadPack>(tempDeadPackList.Values), null, ownRecordAction);
                if (null != recordAction)
                {
@@ -1362,6 +1366,12 @@
            // 取消幻影效果
            caster.ShowIllusionShadow(false);
            // 重置MotionBase的技能动画状态,防止playingSkillWithAnim卡住后续技能
            if (caster is HeroBattleObject heroCaster)
            {
                heroCaster.ForceResetMotionSkillState();
            }
        }
        // 5. 恢复 UI 状态
@@ -1490,14 +1500,22 @@
                skillRecordAction.fromSkill = this;
                currentWaitingSkill.Add(skillRecordAction);
                //  需要给真正parent播的
                //  根据后续技能的属性决定是否需要等待当前技能归位
                RecordAction waitAction = GetFollowUpWaitAction(skillRecordAction);
                if (skillRecordAction.useParentRecordPlayer && skillRecordAction.parentSkillAction != null)
                {
                    skillRecordAction.parentSkillAction.GetInnerRecordPlayer().PlayRecord(skillRecordAction);
                    if (waitAction != null)
                        skillRecordAction.parentSkillAction.GetInnerRecordPlayer().PlayRecord(skillRecordAction, waitAction);
                    else
                        skillRecordAction.parentSkillAction.GetInnerRecordPlayer().PlayRecord(skillRecordAction);
                }
                else
                {
                    ownRecordAction.GetInnerRecordPlayer().PlayRecord(skillRecordAction);
                    if (waitAction != null)
                        ownRecordAction.GetInnerRecordPlayer().PlayRecord(skillRecordAction, waitAction);
                    else
                        ownRecordAction.GetInnerRecordPlayer().PlayRecord(skillRecordAction);
                }
                return false;
@@ -1539,6 +1557,40 @@
        return true;
    }
    /// <summary>
    /// 判定后续技能(追击/连击/反击等)是否需要等待当前技能完成动作后再释放
    /// 综合判别:技能是否有动画、释放模式是否需要位移、是否同一施法者
    /// 仅在确认安全时返回waitAction,避免不必要的等待和潜在的跨角色相互等待
    /// </summary>
    private RecordAction GetFollowUpWaitAction(SkillRecordAction followUp)
    {
        // 当前技能已经归位,无需等待
        if (moveFinished) return null;
        if (followUp.skillBase == null) return null;
        // 后续技能没有动画动作(无SkillMotionName),不涉及位移,无需等待归位
        // 这类技能在CanCastSkill中也不受playingSkillWithAnim限制
        var followUpSkin = followUp.skillBase.skillSkinConfig;
        if (followUpSkin == null || string.IsNullOrEmpty(followUpSkin.SkillMotionName))
            return null;
        // 后续技能原地释放(None/Self),虽有动画但不需要位移到目标,无需等待
        if (followUpSkin.castMode == SkillCastMode.None || followUpSkin.castMode == SkillCastMode.Self)
            return null;
        // 同一施法者的后续技能(连击等):必须等待归位
        // 原因:同一角色的CastToEnemy/CastToTarget会MoveToTarget,
        // 如果上一个技能的DOTween归位还没完成就发起新位移,两个DOTween会冲突
        if (followUp.skillBase.caster == caster)
            return ownRecordAction;
        // 不同施法者的后续技能(追击/反击等):不添加等待
        // 原因:不同角色从各自位置出发,不存在DOTween冲突
        // 注意:此处不添加跨角色等待关系,避免技能链A等B、B等A的相互等待风险
        return null;
    }
    // 添加清理方法:防止内存泄漏
    public virtual void Cleanup()
    {
Main/System/Battle/SkillEffect/BulletSkillEffect.cs
@@ -176,7 +176,7 @@
        RectTransform effectTrans = effectPlayer.transform as RectTransform;
        var bulletCurve = BulletCurveFactory.CreateBulletCurve(caster, skillConfig, skillSkinConfig, effectPlayer, targetTransform, tagUseSkillAttack.HurtList.ToList(), bulletIndex, (index, hitList) =>
        var bulletCurve = BulletCurveFactory.CreateBulletCurve(caster, skillConfig, skillSkinConfig, effectPlayer, targetTransform, HurtListAsList, bulletIndex, (index, hitList) =>
        {
            if (isFinish)
                return;
@@ -384,7 +384,7 @@
        int tempBulletIndex = bulletIndex;
        var bulletCurve = BulletCurveFactory.CreateBulletCurve(caster, skillConfig, skillSkinConfig, effectPlayer, target.GetRectTransform(), tagUseSkillAttack.HurtList.ToList(), bulletIndex, (index, hitList) =>
        var bulletCurve = BulletCurveFactory.CreateBulletCurve(caster, skillConfig, skillSkinConfig, effectPlayer, target.GetRectTransform(), HurtListAsList, bulletIndex, (index, hitList) =>
        {
            if (skillSkinConfig.BulletPath == 4)
            {
Main/System/Battle/SkillEffect/DotSkillEffect.cs
@@ -34,7 +34,7 @@
            target.battleField.battleEffectMgr.PlayEffect(caster, skillSkinConfig.TriggerEffect, target.GetRectTransform(), caster.Camp, target.GetModelScale());
        }
        onHit?.Invoke(0, tagUseSkillAttack.HurtList.ToList());
        onHit?.Invoke(0, HurtListAsList);
        isFinish = true;
    }
Main/System/Battle/SkillEffect/NoEffect.cs
@@ -22,7 +22,7 @@
    public override void OnMiddleFrameEnd(int times, int hitIndex)
    {
        int mainTargetIndex = BattleUtility.GetMainTargetPositionNum(skillBase, caster, tagUseSkillAttack.HurtList.ToList(), skillConfig);
        int mainTargetIndex = BattleUtility.GetMainTargetPositionNum(skillBase, caster, tagUseSkillAttack.HurtList, skillConfig);
        BattleCamp battleCamp = skillConfig.TagFriendly == 1 ? caster.Camp : caster.GetEnemyCamp();
@@ -58,7 +58,7 @@
            }
        }
        onHit?.Invoke(hitIndex, tagUseSkillAttack.HurtList.ToList());
        onHit?.Invoke(hitIndex, HurtListAsList);
    }
    /// <summary>
Main/System/Battle/SkillEffect/NormalSkillEffect.cs
@@ -21,7 +21,7 @@
    public override void OnMiddleFrameEnd(int times, int hitIndex)
    {
        int mainTargetIndex = BattleUtility.GetMainTargetPositionNum(skillBase, caster, tagUseSkillAttack.HurtList.ToList(), skillConfig);
        int mainTargetIndex = BattleUtility.GetMainTargetPositionNum(skillBase, caster, tagUseSkillAttack.HurtList, skillConfig);
        BattleCamp battleCamp = skillConfig.TagFriendly == 1 ? caster.Camp : caster.GetEnemyCamp();
@@ -57,7 +57,7 @@
            }
        }
        onHit?.Invoke(hitIndex, tagUseSkillAttack.HurtList.ToList());
        onHit?.Invoke(hitIndex, HurtListAsList);
    }
    /// <summary>
Main/System/Battle/SkillEffect/SkillEffect.cs
@@ -16,6 +16,18 @@
    protected Action<int, List<HB427_tagSCUseSkill.tagSCUseSkillHurt>> onHit;
    // 缓存HurtList的List副本,避免每次onHit回调时重复ToList分配
    private List<HB427_tagSCUseSkill.tagSCUseSkillHurt> _hurtListCache;
    protected List<HB427_tagSCUseSkill.tagSCUseSkillHurt> HurtListAsList
    {
        get
        {
            if (_hurtListCache == null)
                _hurtListCache = new List<HB427_tagSCUseSkill.tagSCUseSkillHurt>(tagUseSkillAttack.HurtList);
            return _hurtListCache;
        }
    }
    public SkillEffect(SkillBase _skillBase, SkillConfig _skillConfig, SkillSkinConfig _skillSkinConfig, BattleObject _caster, HB427_tagSCUseSkill _tagUseSkillAttack)
    {
        skillBase = _skillBase;
Main/System/Battle/Sound/BattleSoundManager.cs
@@ -22,11 +22,17 @@
    // 记录每个音效ID当前播放的AudioSource
    private Dictionary<int, List<AudioSource>> audioIdToSources = new Dictionary<int, List<AudioSource>>();
    
    // 清理用缓存列表,避免每帧分配
    private List<int> _keysToRemoveCache = new List<int>();
    // 音频剪辑缓存
    private Dictionary<int, AudioClip> audioClipCache = new Dictionary<int, AudioClip>();
    
    // 当前播放速度
    private float currentSpeedRatio = 1f;
    // 跟踪AudioSource总数,避免GetComponents分配
    private int audioSourceCount = 0;
    
    // 是否有焦点
    private bool hasFocus = true;
@@ -111,6 +117,7 @@
            source.spatialBlend = 0f; // 2D音效
            audioSourcePool.Enqueue(source);
        }
        audioSourceCount = INITIAL_AUDIO_POOL;
        Debug.Log($"<color=cyan>BattleSoundManager [{battleField.guid}]: 初始化了 {INITIAL_AUDIO_POOL} 个 AudioSource</color>");
    }
    
@@ -145,21 +152,27 @@
        // 检查是否有焦点,无焦点时不播放
        if (!hasFocus)
        {
#if UNITY_EDITOR
            Debug.Log($"<color=yellow>BattleSoundManager [{battleField.guid}]: 无焦点,拒绝播放音效 {audioId}</color>");
#endif
            return;
        }
        
        // 检查该音效是否已达到播放上限
        if (!CanPlayAudio(audioId))
        {
#if UNITY_EDITOR
            Debug.Log($"<color=yellow>BattleSoundManager [{battleField.guid}]: 音效 {audioId} 达到播放上限,拒绝播放</color>");
#endif
            return;
        }
        
        var audioClip = await GetAudioClip(audioId);
        if (audioClip == null)
        {
#if UNITY_EDITOR
            Debug.Log($"<color=red>BattleSoundManager [{battleField.guid}]: 无法加载音效 {audioId}</color>");
#endif
            return;
        }
        
@@ -167,7 +180,9 @@
        AudioSource source = GetAvailableAudioSource();
        if (source == null)
        {
#if UNITY_EDITOR
            Debug.Log($"<color=red>BattleSoundManager [{battleField.guid}]: 无法获取AudioSource,池数量={audioSourcePool.Count},活跃数量={activeAudioSources.Count}</color>");
#endif
            return;
        }
        
@@ -190,7 +205,9 @@
        source.clip = audioClip;
        source.Play();
        
#if UNITY_EDITOR
        Debug.Log($"<color=green>BattleSoundManager [{battleField.guid}]: 播放音效 {audioId} - {audioClip.name}</color>");
#endif
        
        // 标记为活跃
        if (!activeAudioSources.Contains(source))
@@ -281,16 +298,18 @@
            {
                return source;
            }
#if UNITY_EDITOR
            Debug.Log($"<color=orange>BattleSoundManager [{battleField.guid}]: 池中的AudioSource已被销毁,跳过</color>");
#endif
        }
        
        // 计算当前总的 AudioSource 数量
        int totalAudioSources = audioSourceObject.GetComponents<AudioSource>().Length;
        if (totalAudioSources >= MAX_AUDIO_SOURCES)
        if (audioSourceCount >= MAX_AUDIO_SOURCES)
        {
            // 达到上限,不再创建,丢弃这次播放请求
#if UNITY_EDITOR
            Debug.Log($"BattleSoundManager: AudioSource 数量已达上限 {MAX_AUDIO_SOURCES},无法播放新音效");
#endif
            return null;
        }
        
@@ -299,8 +318,11 @@
        newSource.playOnAwake = false;
        newSource.loop = false;
        newSource.spatialBlend = 0f; // 2D音效
        audioSourceCount++;
        
        Debug.Log($"<color=cyan>BattleSoundManager [{battleField.guid}]: 动态创建新AudioSource,当前总数={totalAudioSources + 1}</color>");
#if UNITY_EDITOR
        Debug.Log($"<color=cyan>BattleSoundManager [{battleField.guid}]: 动态创建新AudioSource,当前总数={audioSourceCount}</color>");
#endif
        
        return newSource;
    }
@@ -328,18 +350,18 @@
        }
        
        // 清理 audioIdToSources 中已停止播放的 AudioSource
        var keysToRemove = new List<int>();
        _keysToRemoveCache.Clear();
        foreach (var kvp in audioIdToSources)
        {
            kvp.Value.RemoveAll(s => s == null || !s.isPlaying);
            if (kvp.Value.Count == 0)
            {
                keysToRemove.Add(kvp.Key);
                _keysToRemoveCache.Add(kvp.Key);
            }
        }
        
        // 移除空的音效ID记录
        foreach (var key in keysToRemove)
        foreach (var key in _keysToRemoveCache)
        {
            audioIdToSources.Remove(key);
        }
@@ -420,11 +442,15 @@
        int activeCount = activeAudioSources.Count;
        int totalStopped = 0;
        
        AudioSource[] allSources = null;
        // 不依赖列表,直接停止 GameObject 上的所有 AudioSource
        if (audioSourceObject != null)
        {
            var allSources = audioSourceObject.GetComponents<AudioSource>();
            allSources = audioSourceObject.GetComponents<AudioSource>();
#if UNITY_EDITOR
            Debug.Log($"<color=red>BattleSoundManager [{battleField.guid}]: StopAllSounds - GameObject上共有 {allSources.Length} 个AudioSource</color>");
#endif
            
            foreach (var source in allSources)
            {
@@ -432,7 +458,9 @@
                {
                    if (source.isPlaying)
                    {
#if UNITY_EDITOR
                        Debug.Log($"<color=red>  停止正在播放的: {source.clip?.name}</color>");
#endif
                        totalStopped++;
                    }
                    source.Stop();
@@ -441,17 +469,18 @@
            }
        }
        
#if UNITY_EDITOR
        Debug.Log($"<color=red>BattleSoundManager [{battleField.guid}]: StopAllSounds - 活跃列表={activeCount}, 实际停止={totalStopped}</color>");
#endif
        
        // 清空所有列表
        activeAudioSources.Clear();
        audioIdToSources.Clear();
        
        // 重建对象池
        // 重建对象池(复用上面已获取的数组)
        audioSourcePool.Clear();
        if (audioSourceObject != null)
        if (allSources != null)
        {
            var allSources = audioSourceObject.GetComponents<AudioSource>();
            foreach (var source in allSources)
            {
                if (source != null)
@@ -461,7 +490,9 @@
            }
        }
        
#if UNITY_EDITOR
        Debug.Log($"<color=red>BattleSoundManager [{battleField.guid}]: StopAllSounds 完成</color>");
#endif
    }
    
    /// <summary>
Main/System/Equip/EquipTipWin.cs
@@ -154,14 +154,14 @@
        bgFlower.color = UIHelper.GetUIColor(itemConfig.ItemColor);
        var baseAttrs = appointItemConfig.BaseAttrID.ToList();
        var baseValues = appointItemConfig.BaseAttrValue.ToList();
        var fightAttrs = appointItemConfig.LegendAttrID.ToList();
        var fightValues = appointItemConfig.LegendAttrValue.ToList();
        var baseAttrs = appointItemConfig.BaseAttrID;
        var baseValues = appointItemConfig.BaseAttrValue;
        var fightAttrs = appointItemConfig.LegendAttrID;
        var fightValues = appointItemConfig.LegendAttrValue;
        for (var i = 0; i < baseAttrNames.Count; i++)
        {
            if (i >= baseAttrs.Count)
            if (i >= baseAttrs.Length)
            {
                baseAttrNames[i].text = "";
                baseAttrValues[i].text = "";
@@ -182,7 +182,7 @@
            fightAttrGameObj.SetActive(true);
            for (var i = 0; i < fightAttrNames.Count; i++)
            {
                if (i >= fightAttrs.Count)
                if (i >= fightAttrs.Length)
                {
                    fightAttrNames[i].SetActive(false);
                }
Main/System/Hero/UIHeroController.cs
@@ -25,6 +25,7 @@
    private static readonly object loadLock = new object();
    private static int lastInitFrame = -1; // 上一次执行Initialize的帧号,用于确保每帧最多1次
    private static GameObjectPoolManager.GameObjectPool cachedUIHeroPool; // 缓存UIHero预制体池
    private RectTransform _instanceRect; // 缓存RectTransform避免重复GetComponent
    public Action onComplete;
    public async UniTask Create(int _skinID, float scale = 0.8f, Action _onComplete = null, string motionName = "idle", bool isLh = false)
@@ -213,8 +214,9 @@
        {
            instanceGO = pool.Request();
            instanceGO.transform.SetParent(transform);
            _instanceRect = instanceGO.GetComponent<RectTransform>();
            //transform 的Pivot Y是0,让instanceGO 居中
            instanceGO.transform.localPosition = new Vector3(0, instanceGO.GetComponent<RectTransform>().sizeDelta.y * 0.5f);
            instanceGO.transform.localPosition = new Vector3(0, _instanceRect.sizeDelta.y * 0.5f);
            //instanceGO.transform.localPosition = Vector3.zero;
            instanceGO.transform.localScale = Vector3.one;
@@ -670,8 +672,9 @@
        {
            instanceGO = pool.Request();
            instanceGO.transform.SetParent(transform);
            _instanceRect = instanceGO.GetComponent<RectTransform>();
            //transform 的Pivot Y是0,让instanceGO 居中
            instanceGO.transform.localPosition = new Vector3(0, instanceGO.GetComponent<RectTransform>().sizeDelta.y * 0.5f);
            instanceGO.transform.localPosition = new Vector3(0, _instanceRect.sizeDelta.y * 0.5f);
            instanceGO.transform.localScale = Vector3.one;
            instanceGO.transform.localRotation = Quaternion.identity;
        }
Main/System/Main/MainWin.cs
@@ -192,7 +192,7 @@
        // 从玩家数据中获取信息并更新UI
        avatarCell.InitUI(AvatarHelper.GetAvatarModel((int)PlayerDatas.Instance.baseData.PlayerID,
                                                        PlayerDatas.Instance.baseData.face,
                                                        PlayerDatas.Instance.baseData.facePic));
                                                        PlayerDatas.Instance.baseData.facePic)).Forget();
        playerNameText.text = PlayerDatas.Instance.baseData.PlayerName;
        powerText.text = UIHelper.ReplaceLargeArtNum(PlayerDatas.Instance.baseData.FightPower);
@@ -214,7 +214,7 @@
            case PlayerDataType.FacePic:
                avatarCell.InitUI(AvatarHelper.GetAvatarModel((int)PlayerDatas.Instance.baseData.PlayerID,
                                                                PlayerDatas.Instance.baseData.face,
                                                                PlayerDatas.Instance.baseData.facePic));
                                                                PlayerDatas.Instance.baseData.facePic)).Forget();
                break;
            case PlayerDataType.default26:
                hammerText.text = UIHelper.GetMoneyCnt(41).ToString();
Main/System/PhantasmPavilion/AvatarCell.cs
@@ -223,10 +223,13 @@
        onLoaded = null;
    }
    public async void InitUI(AvatarModel model)
    public async UniTask InitUI(AvatarModel model)
    {
        if (model == null)
        {
            Debug.LogError("[AvatarCell] model is null");
            return;
        }
        avatarModel = model;
        await LoadPrefab();   //存在被卸载的可能,重新加载
Main/System/Sound/SoundPlayer.cs
@@ -455,7 +455,7 @@
    IEnumerator Co_DelayPlayBackGrondMusic(float _delay, int _audioId)
    {
        yield return new WaitForSeconds(_delay);
        yield return WaitForSecondsCache.Get(_delay);
        PlayBackGroundMusic(_audioId).Forget();
    }
}
Main/System/SpineUpdateManager.cs
@@ -67,15 +67,12 @@
                continue;
            }
            if (!sg.isActiveAndEnabled) continue;
            // 手动驱动动画更新:Update(float)不检查freeze,直接更新动画状态
            sg.Update(dt);
        }
    }
    void LateUpdate()
    {
        // 隔帧更新Mesh:动画状态每帧更新保证事件准确,但Mesh重建(开销大)隔帧即可
        // 数量少时每帧都更新,数量多于8个时隔帧更新以减少CPU负担
        bool skipMesh = managedSpines.Count > 8 && (frameCount & 1) == 0;
        for (int i = managedSpines.Count - 1; i >= 0; i--)
@@ -89,7 +86,6 @@
            }
            if (!sg.isActiveAndEnabled) continue;
            if (skipMesh) continue;
            // 手动驱动Mesh更新:直接调用UpdateMesh(),不经过LateUpdate()的freeze检查
            sg.UpdateMesh();
        }
    }
Main/System/Tip/MarqueeWin.cs
@@ -74,7 +74,7 @@
    IEnumerator Co_StartTween()
    {
        yield return new WaitForSeconds(5f);
        yield return WaitForSecondsCache.Wait5;
        m_ContainerMarquee.SetActive(true);
        BeginMarquee();
    }
Main/System/Tip/ScrollTip.cs
@@ -75,7 +75,7 @@
        if (pool == null)
        {
            var _prefab = await UILoader.LoadPrefabAsync("Tip");
            if (pool != null)
            if (pool == null)
            {
                pool = GameObjectPoolManager.Instance.GetPool(_prefab);
            }
Main/Utility/FrameEffect.cs
@@ -52,7 +52,10 @@
            {
                if (timer > interval)
                {
                    m_Behaviour.overrideSprite = m_Sprites[index];
                    int newIndex = index;
                    // 只在sprite实际变化时才赋值,避免不必要的Image rebuild
                    if (m_Behaviour.overrideSprite != m_Sprites[newIndex])
                        m_Behaviour.overrideSprite = m_Sprites[newIndex];
                    index = (++index) % m_Sprites.Length;
                    timer -= interval;
                }
Main/Utility/UGUIEventListenerContainDrag.cs
@@ -28,12 +28,28 @@
    //是否处于按下状态 与长按配合使用
    bool isDown = false;
    float time = 0;
    private Button _cachedButton;
    private bool _buttonCached;
    private Button CachedButton
    {
        get
        {
            if (!_buttonCached)
            {
                _cachedButton = GetComponent<Button>();
                _buttonCached = true;
            }
            return _cachedButton;
        }
    }
    public void OnPointerClick(PointerEventData eventData)
    {
        if (OnClick != null)
        {
            if (!GetComponent<Button>() || GetComponent<Button>().interactable)
            if (!CachedButton || CachedButton.interactable)
            {
                OnClick(gameObject);
            }
Main/Utility/UIHelper.cs
@@ -1498,10 +1498,10 @@
                serverList.Add(serverName);
                //太长会导致界面顶点数超过65000
                if (serverList.Count > 1000)
                    return string.Join(", ", serverList.ToArray());
                    return string.Join(", ", serverList);
            }
        }
        return string.Join(", ", serverList.ToArray());
        return string.Join(", ", serverList);
    }
    //不同版本现金的单位不一样,比如越南盾是整数,下发是原值;美元和RMB是小数,下发是原值的100
Main/Utility/WaitForSecondsCache.cs
New file
@@ -0,0 +1,28 @@
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// WaitForSeconds缓存,避免每次yield时分配GC
/// 使用方式:yield return WaitForSecondsCache.Get(1f);
/// </summary>
public static class WaitForSecondsCache
{
    private static readonly Dictionary<float, WaitForSeconds> _cache = new Dictionary<float, WaitForSeconds>();
    public static WaitForSeconds Get(float seconds)
    {
        if (!_cache.TryGetValue(seconds, out var wait))
        {
            wait = new WaitForSeconds(seconds);
            _cache[seconds] = wait;
        }
        return wait;
    }
    // 预缓存常用值
    public static readonly WaitForSeconds Wait0_1 = new WaitForSeconds(0.1f);
    public static readonly WaitForSeconds Wait0_5 = new WaitForSeconds(0.5f);
    public static readonly WaitForSeconds Wait1 = new WaitForSeconds(1f);
    public static readonly WaitForSeconds Wait2 = new WaitForSeconds(2f);
    public static readonly WaitForSeconds Wait5 = new WaitForSeconds(5f);
}
Main/Utility/WaitForSecondsCache.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: de580f55531ab54439b4e279b87951e7
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant: