using System; using System.Collections.Generic; using UnityEngine; using Spine.Unity; public class MotionBase { public float MotionTimeScale = 1f; public static List AttackMotionList = new List { MotionName.attack.ToString().ToLower(), MotionName.angerSkill.ToString().ToLower(), MotionName.passiveSkill.ToString().ToLower(), }; private Dictionary trackEntryCallbacks = new Dictionary(); public Action OnAttackAnimationComplete; public Action OnHitAnimationComplete; private List runningActions = new List(); public SkeletonAnimation skeletonAnim; protected Spine.AnimationState animState; protected Spine.Skeleton skeleton; protected float defaultMixDuration = 0f; private Spine.TrackEntry currentTrack; private Dictionary activeSkillTracks = new Dictionary(); // 子技能轨道池管理(在Init中初始化,不要在这里初始化) private Queue availableSubTracks; private Dictionary subSkillTrackMap = new Dictionary(); private SkeletonIllusionShadow illusionShadow; private bool playingSkillAnim = false; private bool isUnderControl = false; private float pauseTime = 0f; private float resumeTime = 0f; // 新增:累积暂停时长,用于对非动画路径(Time.time)进行稳定的时间修正 private float pausedAccumulated = 0f; private float pauseStart = 0f; public virtual void Init(SkeletonAnimation skelAnim) { skeletonAnim = skelAnim; if (skeletonAnim == null) { BattleDebug.LogError("缺少SkeletonGraphic组件!"); return; } animState = skeletonAnim.AnimationState; skeletonAnim.timeScale = MotionTimeScale; skeleton = skeletonAnim.Skeleton; if (animState != null) animState.Data.DefaultMix = defaultMixDuration; // 初始化子技能轨道池 availableSubTracks = new Queue(); for (int i = 1; i <= 8; i++) availableSubTracks.Enqueue(i); PlayAnimation(MotionName.idle, true); SetupAnimationHandlers(); if (skelAnim.gameObject != null) illusionShadow = skelAnim.gameObject.AddMissingComponent(); } public virtual void Release() { trackEntryCallbacks.Clear(); activeSkillTracks.Clear(); availableSubTracks?.Clear(); subSkillTrackMap.Clear(); if (animState != null) { animState.Complete -= OnAnimationComplete; animState.ClearTracks(); animState = null; } skeletonAnim = null; skeleton = null; currentTrack = null; playingSkillAnim = false; } public virtual Spine.TrackEntry PlayAnimation(MotionName motionName, bool loop, Action onComplete = null) { if (playingSkillAnim || animState == null) return null; if (currentTrack != null && !currentTrack.IsComplete && trackEntryCallbacks.TryGetValue(currentTrack, out var prevCallback)) { trackEntryCallbacks.Remove(currentTrack); prevCallback?.Invoke(); currentTrack = null; } currentTrack = animState.SetAnimation(0, motionName.ToString(), loop); if (onComplete != null && currentTrack != null) trackEntryCallbacks[currentTrack] = onComplete; return currentTrack; } private void AddAction(Action action) => runningActions.Add(action); private void RemoveAction(Action action) => runningActions.Remove(action); public Spine.TrackEntry PlaySkillAnimation(SkillConfig skillConfig, SkillBase skillBase, bool isSubSkill, Action onComplete = null) { if (skillConfig == null) { Debug.LogError("技能配置为空,无法播放技能动画"); return null; } if (animState == null || skeleton == null) { Debug.LogError("SkeletonGraphic或AnimationState未初始化,无法播放技能动画"); return null; } // 如果没有动画名称,使用无动画模式 if (string.IsNullOrEmpty(skillConfig.SkillMotionName)) { PlaySkillNoAnim(skillConfig, skillBase, onComplete, isSubSkill); return null; } Spine.Animation targetAnim = FindAnim(skillConfig.SkillMotionName); if (targetAnim == null) { skillBase.ForceFinished(); return null; } return ExecuteSkillAnim(skillConfig, skillBase, onComplete, targetAnim, true, isSubSkill); } private Spine.TrackEntry ExecuteSkillAnim(SkillConfig skillConfig, SkillBase skillBase, Action onComplete, Spine.Animation targetAnim, bool hasAnim, bool isSubSkill) { bool isPangdeSkill = 1003020 == skillConfig.SkillID; int loopCount = skillConfig.LoopCount; int[] activeFrames = skillConfig.ActiveFrames ?? new int[0]; int frameCount = activeFrames.Length; float recoveryFrame = skillConfig.RecoveryFrames; // 轨道分配策略:主技能用 track 0,子技能从轨道池分配 int trackIndex = 0; if (isSubSkill) { if (availableSubTracks != null && availableSubTracks.Count > 0) { trackIndex = availableSubTracks.Dequeue(); subSkillTrackMap[skillBase] = trackIndex; } else { // 轨道池耗尽或未初始化,回退到无动画模式 Debug.LogWarning($"子技能轨道池已满或未初始化,技能{skillConfig.SkillID}使用无动画模式"); PlaySkillNoAnim(skillConfig, skillBase, onComplete, isSubSkill); return null; } } Spine.TrackEntry skillTrack = null; if (hasAnim) { skillTrack = animState.SetAnimation(trackIndex, targetAnim, false); if (null == skillTrack) { Debug.LogError($"技能 {skillConfig.SkillID} 动画设置失败"); // 如果是子技能且分配了轨道,需要回收 if (isSubSkill && subSkillTrackMap.ContainsKey(skillBase)) { if (availableSubTracks != null) availableSubTracks.Enqueue(subSkillTrackMap[skillBase]); subSkillTrackMap.Remove(skillBase); } return null; } // 只有主技能才更新 currentTrack if (!isSubSkill) currentTrack = skillTrack; activeSkillTracks[trackIndex] = skillTrack; } playingSkillAnim = true; int currentLoop = 0, triggerCount = 0, failCount = 0; bool beginTriggered = false, finalStarted = false, finalEnded = false, middleStarted = false; bool[] triggeredFrames = new bool[frameCount]; // 新增:记录技能开始时的 pausedAccumulated 基线,用于后续计算该技能自身的暂停时长 float pausedAccumulatedAtStart = pausedAccumulated; // startTime 表示技能"本地逻辑时间"的起点(以 Time.time 为基准) float startTime = hasAnim ? 0f : Time.time; skillBase.OnSkillStart(); Action frameHandler = null; frameHandler = () => { if (skillBase.IsFinished()) { // 清理并退出(保证状态一致) if (skillTrack != null && activeSkillTracks.ContainsKey(trackIndex)) { if (activeSkillTracks[trackIndex] == skillTrack) activeSkillTracks.Remove(trackIndex); } // 回收子技能轨道 if (isSubSkill && subSkillTrackMap.ContainsKey(skillBase)) { if (availableSubTracks != null) availableSubTracks.Enqueue(subSkillTrackMap[skillBase]); subSkillTrackMap.Remove(skillBase); } // 只有当没有其他活跃技能时才复位 playingSkillAnim if (activeSkillTracks.Count == 0) playingSkillAnim = false; RemoveAction(frameHandler); return; } float trackTime = 0f; if (hasAnim) { trackTime = skillTrack.TrackTime; } else { // 使用 pausedAccumulatedAtStart 来计算"这个技能自开始以来的暂停总时长" float adjustedTime = Time.time; float thisSkillPaused = pausedAccumulated - pausedAccumulatedAtStart; if (thisSkillPaused < 0f) thisSkillPaused = 0f; // 保险防护 // 逻辑运行时间 = 当前时间 - startTime - 本技能已暂停时长 trackTime = (adjustedTime - startTime - thisSkillPaused) * MotionTimeScale; } float currentFrame = trackTime * BattleConst.skillMotionFps; if (hasAnim) { // 检查当前轨道是否被新技能覆盖 if (!activeSkillTracks.ContainsKey(trackIndex) || activeSkillTracks[trackIndex] != skillTrack) { Debug.LogError("技能动画被打断,强制结束 " + skillConfig.SkillID); skillBase.ForceFinished(); // 清理并确保状态复位 RemoveAction(frameHandler); // 回收子技能轨道 if (isSubSkill && subSkillTrackMap.ContainsKey(skillBase)) { if (availableSubTracks != null) availableSubTracks.Enqueue(subSkillTrackMap[skillBase]); subSkillTrackMap.Remove(skillBase); } if (activeSkillTracks.Count == 0) playingSkillAnim = false; return; } if (skillTrack.TrackTime == 0) failCount++; if (failCount > 100) { Debug.LogError("技能动画播放失败,强制结束 " + skillConfig.SkillID); skillBase.ForceFinished(); RemoveAction(frameHandler); if (activeSkillTracks.ContainsKey(trackIndex)) activeSkillTracks.Remove(trackIndex); // 回收子技能轨道 if (isSubSkill && subSkillTrackMap.ContainsKey(skillBase)) { if (availableSubTracks != null) availableSubTracks.Enqueue(subSkillTrackMap[skillBase]); subSkillTrackMap.Remove(skillBase); } if (activeSkillTracks.Count == 0) playingSkillAnim = false; return; } } // 各阶段回调(原有逻辑) if (!beginTriggered && currentFrame >= skillConfig.StartupFrames && currentLoop == 0) { beginTriggered = true; skillBase.OnStartSkillFrameEnd(); } if (!middleStarted && currentFrame >= skillConfig.StartupFrames && currentLoop <= loopCount) { middleStarted = true; skillBase.OnMiddleFrameStart(currentLoop); } for (int i = 0; i < frameCount && i < triggeredFrames.Length; i++) { if (!triggeredFrames[i] && currentFrame >= activeFrames[i]) { skillBase.OnMiddleFrameEnd(currentLoop, triggerCount++); triggeredFrames[i] = true; } } bool allTriggered = Array.TrueForAll(triggeredFrames, x => x); if (allTriggered && currentLoop < loopCount) { currentLoop++; Array.Clear(triggeredFrames, 0, frameCount); middleStarted = false; if (currentLoop < loopCount) { if (BattleConst.skillMotionFps > 0) { if (hasAnim) { skillTrack.TrackTime = skillConfig.StartupFrames / BattleConst.skillMotionFps; } else { // 为下一 loop 重置 startTime,并且更新 pausedAccumulatedAtStart(以保持基线) startTime = Time.time - (skillConfig.StartupFrames / BattleConst.skillMotionFps); pausedAccumulatedAtStart = pausedAccumulated; } } beginTriggered = false; } else { finalStarted = false; finalEnded = false; } } if (currentLoop >= loopCount) { if (!finalStarted && currentFrame >= recoveryFrame) { finalStarted = true; skillBase.OnFinalFrameStart(); } if (finalStarted && !finalEnded && currentFrame >= recoveryFrame) { finalEnded = true; // 清理技能轨道 if (skillTrack != null && activeSkillTracks.ContainsKey(trackIndex)) { if (activeSkillTracks[trackIndex] == skillTrack) activeSkillTracks.Remove(trackIndex); } // 回收子技能轨道 if (isSubSkill && subSkillTrackMap.ContainsKey(skillBase)) { if (availableSubTracks != null) availableSubTracks.Enqueue(subSkillTrackMap[skillBase]); subSkillTrackMap.Remove(skillBase); } // 只有当没有其他活跃技能时才复位 playingSkillAnim if (activeSkillTracks.Count == 0) playingSkillAnim = false; RemoveAction(frameHandler); onComplete?.Invoke(); skillBase.OnFinalFrameEnd(); } } }; AddAction(frameHandler); return skillTrack; } private Spine.Animation FindAnim(string animName) { if (string.IsNullOrEmpty(animName)) return null; Spine.Animation targetAnim = skeleton.Data.FindAnimation(animName); if (targetAnim == null && skeleton.Data.Animations != null) { for (int i = 0; i < skeleton.Data.Animations.Count; i++) { var anim = skeleton.Data.Animations.Items[i]; if (anim?.Name != null && anim.Name.ToLower() == animName.ToLower()) { targetAnim = anim; break; } } } if (targetAnim == null) Debug.LogError($"找不到动画: {animName}"); return targetAnim; } private void PlaySkillNoAnim(SkillConfig skillConfig, SkillBase skillBase, Action onComplete, bool isSubSkill) => ExecuteSkillAnim(skillConfig, skillBase, onComplete, null, false, isSubSkill); protected virtual void SetupAnimationHandlers() { if (animState != null) animState.Complete += OnAnimationComplete; } protected virtual void OnAnimationComplete(Spine.TrackEntry trackEntry) { if (trackEntry?.Animation?.Name == null) return; string animName = trackEntry.Animation.Name.ToLower(); if (AttackMotionList.Contains(animName)) { OnAttackAnimationComplete?.Invoke(); PlayAnimation(MotionName.idle, true); } else if (animName == MotionName.hit.ToString().ToLower()) { OnHitAnimationComplete?.Invoke(); PlayAnimation(MotionName.idle, true); } if (trackEntryCallbacks.TryGetValue(trackEntry, out var callback)) { trackEntryCallbacks.Remove(trackEntry); callback?.Invoke(); } } public void SetControledAnimation() { // 受到硬控的时候,保持受击动画的第三帧,直到控制结束 或者最后一击死亡,移除控制效果后,恢复到待机状态或者播放死亡动画 // 这里是受到硬控时候 需要表现的动画 var entry = PlayAnimation(MotionName.hit, false); float threeFrameTrackTime = 3f / BattleConst.skillMotionFps; entry.TrackTime = threeFrameTrackTime; entry.TimeScale = 0; isUnderControl = true; } public void CancelControledAnimation() { // 硬控结束,恢复动画播放 isUnderControl = false; PlayAnimation(MotionName.idle, true); } public virtual void Run() { for (int i = runningActions.Count - 1; i >= 0; i--) runningActions[i]?.Invoke(); illusionShadow?.Run(); } public virtual void Pause() { if (skeletonAnim != null) skeletonAnim.timeScale = 0f; // 记录单次暂停开始时刻 pauseStart = Time.time; // 兼容旧字段(保留但不再用于累积逻辑) pauseTime = Time.time; } public virtual void Resume() { if (skeletonAnim != null) skeletonAnim.timeScale = MotionTimeScale; // 累积暂停时长(如果曾记录过 pauseStart) if (pauseStart > 0f) { pausedAccumulated += (Time.time - pauseStart); pauseStart = 0f; } // 保持旧字段以兼容现有代码(但实际时间修正使用 pausedAccumulated) resumeTime = Time.time; } public void HaveRest() { trackEntryCallbacks.Clear(); runningActions.Clear(); activeSkillTracks.Clear(); // 重置子技能轨道池 if (availableSubTracks == null) availableSubTracks = new Queue(); else availableSubTracks.Clear(); subSkillTrackMap.Clear(); for (int i = 1; i <= 8; i++) availableSubTracks.Enqueue(i); playingSkillAnim = false; PlayAnimation(MotionName.idle, true); } public void SetSpeedRatio(float ratio) { MotionTimeScale = ratio; if (skeletonAnim != null) skeletonAnim.timeScale = ratio; } public void ShowIllusionShadow(bool isVisible, Color color = default) { if (illusionShadow != null) { illusionShadow.SetSkeletonAnimation(skeletonAnim); illusionShadow.Show(isVisible, color); } } }