| | |
| | | |
| | | IEnumerator DelayClick() |
| | | { |
| | | yield return new WaitForSeconds(interval); |
| | | yield return WaitForSecondsCache.Get(interval); |
| | | targetBtn.enabled = true; |
| | | } |
| | | } |
| | |
| | | if (this.gameObject.activeInHierarchy) |
| | | { |
| | | SetStartState(); |
| | | StartCoroutine("Co_StartTween"); |
| | | StartCoroutine(Co_StartTween()); |
| | | } |
| | | } |
| | | |
| | |
| | | SetStartState(); |
| | | } |
| | | |
| | | StartCoroutine("Co_StartTween"); |
| | | StartCoroutine(Co_StartTween()); |
| | | } |
| | | } |
| | | |
| | |
| | | if (this.gameObject.activeInHierarchy) |
| | | { |
| | | SetStartState(); |
| | | StartCoroutine("Co_StartTween"); |
| | | StartCoroutine(Co_StartTween()); |
| | | } |
| | | } |
| | | |
| | |
| | | if (trigger == Trigger.Start) |
| | | { |
| | | SetStartState(); |
| | | StartCoroutine("Co_StartTween"); |
| | | StartCoroutine(Co_StartTween()); |
| | | } |
| | | } |
| | | |
| | |
| | | if (trigger == Trigger.Enable) |
| | | { |
| | | SetStartState(); |
| | | StartCoroutine("Co_StartTween"); |
| | | StartCoroutine(Co_StartTween()); |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | 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; |
| | |
| | | case DelayMode.Time: |
| | | if (delay > 0.001f) |
| | | { |
| | | yield return new WaitForSeconds(delay); |
| | | yield return WaitForSecondsCache.Get(delay); |
| | | } |
| | | break; |
| | | } |
| | |
| | | if (this.gameObject.activeInHierarchy) |
| | | { |
| | | SetStartState(); |
| | | StartCoroutine("Co_StartTween"); |
| | | StartCoroutine(Co_StartTween()); |
| | | } |
| | | } |
| | | |
| | |
| | | SetStartState(); |
| | | } |
| | | |
| | | StartCoroutine("Co_StartTween"); |
| | | StartCoroutine(Co_StartTween()); |
| | | } |
| | | } |
| | | |
| | |
| | | if (this.gameObject.activeInHierarchy) |
| | | { |
| | | SetStartState(); |
| | | StartCoroutine("Co_StartTween"); |
| | | StartCoroutine(Co_StartTween()); |
| | | } |
| | | } |
| | | |
| | |
| | | if (trigger == Trigger.Start) |
| | | { |
| | | SetStartState(); |
| | | StartCoroutine("Co_StartTween"); |
| | | StartCoroutine(Co_StartTween()); |
| | | } |
| | | } |
| | | |
| | |
| | | if (trigger == Trigger.Enable) |
| | | { |
| | | SetStartState(); |
| | | StartCoroutine("Co_StartTween"); |
| | | StartCoroutine(Co_StartTween()); |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | 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; |
| | |
| | | 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 |
| | | { |
| | |
| | | // 清除之前的列表 |
| | | 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; |
| | |
| | | CreateChildMaskMaterial(graphic); |
| | | m_MaskedChildren.Add(graphic); |
| | | } |
| | | _graphicCacheList.Clear(); |
| | | } |
| | | |
| | | /// <summary> |
| | |
| | | { |
| | | 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) |
| | | { |
| | |
| | | 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++) |
| | | { |
| | |
| | | { |
| | | // Determine play mode based on AssetSource setting |
| | | EPlayMode playMode; |
| | | YooAsset.IRemoteServices remoteServices = null; |
| | | if (!AssetSource.isUseAssetBundle) |
| | | { |
| | | // Editor 不使用 AB 模式:EditorSimulateMode 直接读 AssetDatabase |
| | |
| | | #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; |
| | |
| | | } |
| | | |
| | | // 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); |
| | |
| | | // WebSocket 实现(WebGL平台) |
| | | WebSocket webSocket; |
| | | public WebSocket socket { get { return webSocket; } } |
| | | private byte[] fragmentBytes; // TCP-to-WS网关按TCP缓冲区拆包,需要跨消息重组 |
| | | #endif |
| | | |
| | | public Action OnDisconnected; |
| | |
| | | { |
| | | 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; |
| | |
| | | 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; |
| | | } |
| | | |
| | |
| | | public async void CloseConnect() |
| | | { |
| | | Debug.Log("[ClientSocket-WebSocket] ==== CloseConnect"); |
| | | fragmentBytes = null; |
| | | |
| | | if (webSocket != null) |
| | | { |
| | |
| | | // 当前最高的排序顺序 |
| | | 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; |
| | | |
| | |
| | | // 重置当前最高排序顺序 |
| | | 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); |
| | |
| | | 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; |
| | | } |
| | |
| | | } |
| | | } |
| | | |
| | | yield return new WaitForSeconds(1f); |
| | | yield return WaitForSecondsCache.Wait1; |
| | | // yield return WaitingForSecondConst.WaitMS1000; |
| | | } |
| | | } |
| | |
| | | |
| | | 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); |
| | |
| | | |
| | | 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(); |
| | |
| | | |
| | | 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]; |
| | |
| | | |
| | | public class SkillRecordAction : RecordAction |
| | | { |
| | | #if UNITY_EDITOR |
| | | // #if UNITY_EDITOR |
| | | public |
| | | #else |
| | | protected |
| | | #endif |
| | | // #else |
| | | // protected |
| | | // #endif |
| | | |
| | | |
| | | SkillBase skillBase; |
| | |
| | | |
| | | // 同时只能有一场战斗在进行 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 |
| | |
| | | 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(); |
| | |
| | | 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>(); |
| | | |
| | |
| | | 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); |
| | |
| | | battleObj.SetSpeedRatio(battleField.speedRatio); |
| | | } |
| | | } |
| | | MarkCampDirty(campDict); |
| | | |
| | | if (teamBase.teamMingge != null) |
| | | { |
| | |
| | | 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); |
| | |
| | | } |
| | | } |
| | | campDict.Clear(); |
| | | MarkCampDirty(campDict); |
| | | } |
| | | |
| | | // 空闲状态 |
| | |
| | | { |
| | | return motionBase.CanCastSkill(skillSkinConfig); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 强制重置MotionBase的技能动画状态 |
| | | /// 用于技能ForceFinish后防止playingSkillWithAnim卡住后续技能 |
| | | /// </summary> |
| | | public void ForceResetMotionSkillState() |
| | | { |
| | | motionBase?.ForceResetSkillState(); |
| | | } |
| | | |
| | | public override SkeletonAnimation GetSkeletonAnimation() |
| | | { |
| | |
| | | |
| | | 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(); |
| | |
| | | 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 |
| | | { |
| | |
| | | { 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; |
| | |
| | | |
| | | public void Run() |
| | | { |
| | | List<int> removeEffectList = new List<int>(); |
| | | _removeEffectCache.Clear(); |
| | | // 跟随BattleObject |
| | | foreach (var kv in buffEffectDict) |
| | | { |
| | |
| | | if (effectPlayer.isBindBone) |
| | | { |
| | | effectPlayer.FollowBoneXY(); |
| | | return; |
| | | continue; |
| | | } |
| | | int[] effectPos = effectPlayer.effectConfig.effectPos; |
| | | effectPlayer.transform.position = battleObject.GetPosition(); |
| | |
| | | } |
| | | else |
| | | { |
| | | removeEffectList.Add(kv.Key); |
| | | _removeEffectCache.Add(kv.Key); |
| | | } |
| | | } |
| | | |
| | | foreach (var effectId in removeEffectList) |
| | | foreach (var effectId in _removeEffectCache) |
| | | { |
| | | buffEffectDict.Remove(effectId); |
| | | } |
| | |
| | | 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) |
| | |
| | | |
| | | 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; |
| | |
| | | |
| | | 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; |
| | | } |
| | | |
| | |
| | | { |
| | | 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; |
| | | } |
| | | } |
| | | } |
| | |
| | | // 自身动作是否完成(不包括子节点的完成状态) |
| | | 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 |
| | | // 这样可以保证: |
| | |
| | | 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) |
| | |
| | | |
| | | 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()) |
| | |
| | | |
| | | 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()) |
| | |
| | | |
| | | } |
| | | |
| | | // 技能运行主逻辑:处理技能效果和其他技能动作 |
| | | // 技能运行主逻辑:仅驱动技能效果(skillEffect),子技能和死亡由IsFinished()推进 |
| | | public virtual void Run() |
| | | { |
| | | if (skillEffect != null) |
| | |
| | | } |
| | | return; |
| | | } |
| | | |
| | | |
| | | } |
| | | |
| | | protected void ShadowIllutionCreate(bool create) |
| | |
| | | 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); |
| | | |
| | |
| | | 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); |
| | | } |
| | |
| | | // 确保施法者也被高亮(原逻辑) |
| | | 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); |
| | |
| | | // 获取并分配掉落物品和经验 |
| | | 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); |
| | |
| | | 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() |
| | | { |
| | |
| | | return false; |
| | | } |
| | | |
| | | // 检查技能是否完成:综合检查所有完成条件 |
| | | // 检查技能是否完成:同时推进状态(清理完成的子技能、处理剩余包、触发死亡判定) |
| | | // 注意:此方法有副作用,这是设计使然——由SkillRecordAction.Run()每帧调用来驱动状态推进 |
| | | public virtual bool IsFinished() |
| | | { |
| | | if (!isPlay) return false; |
| | |
| | | tempRetValue = false; |
| | | } |
| | | |
| | | // 检查其他技能动作是否完成 |
| | | // 检查跟进的技能动作是否完成(追击/连击/反击等) |
| | | if (currentWaitingSkill.Count > 0) |
| | | { |
| | | if (currentWaitingSkill.Any(s => s.IsFinished())) |
| | |
| | | return false; |
| | | } |
| | | |
| | | |
| | | // 检查最终完成状态 |
| | | if (isFinished && moveFinished) |
| | | { |
| | |
| | | return false; |
| | | } |
| | | |
| | | // 如果自己内部的recora action的 inner record player还有没执行完的包 也是返回false |
| | | // 如果自己内部的 innerRecordPlayer 还有没执行完的包 也是返回false |
| | | if (ownRecordAction != null && ownRecordAction.GetInnerRecordPlayer().IsPlaying()) |
| | | { |
| | | return false; |
| | |
| | | { |
| | | battleField.RemoveCastingSkill(caster.ObjID, this); |
| | | |
| | | // 传递parentRecordAction,让死亡技能等待当前技能完成 |
| | | DeathRecordAction recordAction = battleField.OnObjsDead(new List<BattleDeadPack>(tempDeadPackList.Values), null, ownRecordAction); |
| | | if (null != recordAction) |
| | | { |
| | |
| | | |
| | | // 取消幻影效果 |
| | | caster.ShowIllusionShadow(false); |
| | | |
| | | // 重置MotionBase的技能动画状态,防止playingSkillWithAnim卡住后续技能 |
| | | if (caster is HeroBattleObject heroCaster) |
| | | { |
| | | heroCaster.ForceResetMotionSkillState(); |
| | | } |
| | | } |
| | | |
| | | // 5. 恢复 UI 状态 |
| | |
| | | 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; |
| | |
| | | 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() |
| | | { |
| | |
| | | |
| | | 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; |
| | |
| | | |
| | | 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) |
| | | { |
| | |
| | | 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; |
| | | } |
| | | |
| | |
| | | |
| | | 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(); |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | onHit?.Invoke(hitIndex, tagUseSkillAttack.HurtList.ToList()); |
| | | onHit?.Invoke(hitIndex, HurtListAsList); |
| | | } |
| | | |
| | | /// <summary> |
| | |
| | | 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(); |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | onHit?.Invoke(hitIndex, tagUseSkillAttack.HurtList.ToList()); |
| | | onHit?.Invoke(hitIndex, HurtListAsList); |
| | | } |
| | | |
| | | /// <summary> |
| | |
| | | |
| | | 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; |
| | |
| | | // 记录每个音效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; |
| | |
| | | source.spatialBlend = 0f; // 2D音效 |
| | | audioSourcePool.Enqueue(source); |
| | | } |
| | | audioSourceCount = INITIAL_AUDIO_POOL; |
| | | Debug.Log($"<color=cyan>BattleSoundManager [{battleField.guid}]: 初始化了 {INITIAL_AUDIO_POOL} 个 AudioSource</color>"); |
| | | } |
| | | |
| | |
| | | // 检查是否有焦点,无焦点时不播放 |
| | | 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; |
| | | } |
| | | |
| | |
| | | AudioSource source = GetAvailableAudioSource(); |
| | | if (source == null) |
| | | { |
| | | #if UNITY_EDITOR |
| | | Debug.Log($"<color=red>BattleSoundManager [{battleField.guid}]: 无法获取AudioSource,池数量={audioSourcePool.Count},活跃数量={activeAudioSources.Count}</color>"); |
| | | #endif |
| | | return; |
| | | } |
| | | |
| | |
| | | source.clip = audioClip; |
| | | source.Play(); |
| | | |
| | | #if UNITY_EDITOR |
| | | Debug.Log($"<color=green>BattleSoundManager [{battleField.guid}]: 播放音效 {audioId} - {audioClip.name}</color>"); |
| | | #endif |
| | | |
| | | // 标记为活跃 |
| | | if (!activeAudioSources.Contains(source)) |
| | |
| | | { |
| | | 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; |
| | | } |
| | | |
| | |
| | | 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; |
| | | } |
| | |
| | | } |
| | | |
| | | // 清理 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); |
| | | } |
| | |
| | | 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) |
| | | { |
| | |
| | | { |
| | | if (source.isPlaying) |
| | | { |
| | | #if UNITY_EDITOR |
| | | Debug.Log($"<color=red> 停止正在播放的: {source.clip?.name}</color>"); |
| | | #endif |
| | | totalStopped++; |
| | | } |
| | | source.Stop(); |
| | |
| | | } |
| | | } |
| | | |
| | | #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) |
| | |
| | | } |
| | | } |
| | | |
| | | #if UNITY_EDITOR |
| | | Debug.Log($"<color=red>BattleSoundManager [{battleField.guid}]: StopAllSounds 完成</color>"); |
| | | #endif |
| | | } |
| | | |
| | | /// <summary> |
| | |
| | | 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 = "";
|
| | |
| | | fightAttrGameObj.SetActive(true);
|
| | | for (var i = 0; i < fightAttrNames.Count; i++)
|
| | | {
|
| | | if (i >= fightAttrs.Count)
|
| | | if (i >= fightAttrs.Length)
|
| | | {
|
| | | fightAttrNames[i].SetActive(false);
|
| | | }
|
| | |
| | | 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) |
| | |
| | | { |
| | | 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; |
| | |
| | | { |
| | | 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; |
| | | } |
| | |
| | | // 从玩家数据中获取信息并更新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); |
| | |
| | | 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(); |
| | |
| | | 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(); //存在被卸载的可能,重新加载
|
| | |
|
| | |
| | | |
| | | IEnumerator Co_DelayPlayBackGrondMusic(float _delay, int _audioId) |
| | | { |
| | | yield return new WaitForSeconds(_delay); |
| | | yield return WaitForSecondsCache.Get(_delay); |
| | | PlayBackGroundMusic(_audioId).Forget(); |
| | | } |
| | | } |
| | |
| | | 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--) |
| | |
| | | } |
| | | if (!sg.isActiveAndEnabled) continue; |
| | | if (skipMesh) continue; |
| | | // 手动驱动Mesh更新:直接调用UpdateMesh(),不经过LateUpdate()的freeze检查 |
| | | sg.UpdateMesh(); |
| | | } |
| | | } |
| | |
| | | |
| | | IEnumerator Co_StartTween() |
| | | { |
| | | yield return new WaitForSeconds(5f); |
| | | yield return WaitForSecondsCache.Wait5; |
| | | m_ContainerMarquee.SetActive(true); |
| | | BeginMarquee(); |
| | | } |
| | |
| | | if (pool == null) |
| | | { |
| | | var _prefab = await UILoader.LoadPrefabAsync("Tip"); |
| | | if (pool != null) |
| | | if (pool == null) |
| | | { |
| | | pool = GameObjectPoolManager.Instance.GetPool(_prefab); |
| | | } |
| | |
| | | { |
| | | 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; |
| | | } |
| | |
| | | //是否处于按下状态 与长按配合使用 |
| | | 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); |
| | | } |
| | |
| | | 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 |
| New file |
| | |
| | | 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); |
| | | } |
| New file |
| | |
| | | fileFormatVersion: 2 |
| | | guid: de580f55531ab54439b4e279b87951e7 |
| | | MonoImporter: |
| | | externalObjects: {} |
| | | serializedVersion: 2 |
| | | defaultReferences: [] |
| | | executionOrder: 0 |
| | | icon: {instanceID: 0} |
| | | userData: |
| | | assetBundleName: |
| | | assetBundleVariant: |