| | |
| | | |
| | | public class MotionBase |
| | | { |
| | | public static float MotionTimeScale = 1f; |
| | | public float MotionTimeScale = 1f; |
| | | public static List<string> AttackMotionList = new List<string> |
| | | { |
| | | MotionName.attack.ToString().ToLower(), |
| | |
| | | protected Spine.Skeleton skeleton; |
| | | protected float defaultMixDuration = 0f; |
| | | private Spine.TrackEntry currentTrack; |
| | | private Dictionary<int, Spine.TrackEntry> activeSkillTracks = new Dictionary<int, Spine.TrackEntry>(); |
| | | |
| | | // 子技能轨道池管理(在Init中初始化,不要在这里初始化) |
| | | private Queue<int> availableSubTracks; |
| | | private Dictionary<SkillBase, int> subSkillTrackMap = new Dictionary<SkillBase, int>(); |
| | | |
| | | 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) |
| | | { |
| | |
| | | } |
| | | |
| | | animState = skeletonAnim.AnimationState; |
| | | animState.TimeScale = MotionTimeScale; |
| | | skeletonAnim.timeScale = MotionTimeScale; |
| | | skeleton = skeletonAnim.Skeleton; |
| | | |
| | | if (animState != null) |
| | | animState.Data.DefaultMix = defaultMixDuration; |
| | | |
| | | // 初始化子技能轨道池 |
| | | availableSubTracks = new Queue<int>(); |
| | | for (int i = 1; i <= 8; i++) |
| | | availableSubTracks.Enqueue(i); |
| | | |
| | | PlayAnimation(MotionName.idle, true); |
| | | SetupAnimationHandlers(); |
| | |
| | | public virtual void Release() |
| | | { |
| | | trackEntryCallbacks.Clear(); |
| | | activeSkillTracks.Clear(); |
| | | availableSubTracks?.Clear(); |
| | | subSkillTrackMap.Clear(); |
| | | if (animState != null) |
| | | { |
| | | animState.Complete -= OnAnimationComplete; |
| | |
| | | return null; |
| | | } |
| | | |
| | | // 如果没有动画名称,使用无动画模式 |
| | | if (string.IsNullOrEmpty(skillConfig.SkillMotionName)) |
| | | { |
| | | PlaySkillNoAnim(skillConfig, skillBase, onComplete, 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(0, targetAnim, false); |
| | | currentTrack = skillTrack; |
| | | 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]; |
| | | float startTime = hasAnim ? 0 : Time.time; |
| | | |
| | | // 新增:记录技能开始时的 pausedAccumulated 基线,用于后续计算该技能自身的暂停时长 |
| | | float pausedAccumulatedAtStart = pausedAccumulated; |
| | | |
| | | // startTime 表示技能"本地逻辑时间"的起点(以 Time.time 为基准) |
| | | float startTime = hasAnim ? 0f : Time.time; |
| | | |
| | | skillBase.OnSkillStart(); |
| | | |
| | |
| | | { |
| | | if (skillBase.IsFinished()) |
| | | { |
| | | playingSkillAnim = false; |
| | | // 清理并退出(保证状态一致) |
| | | 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 currentFrame = 0f; |
| | | if (BattleConst.skillMotionFps > 0) |
| | | currentFrame = hasAnim ? (skillTrack.TrackTime * skillTrack.TimeScale * BattleConst.skillMotionFps) : ((Time.time - startTime) * MotionTimeScale * BattleConst.skillMotionFps); |
| | | float trackTime = 0f; |
| | | |
| | | if (hasAnim) |
| | | { |
| | | if (currentTrack != skillTrack) |
| | | 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); |
| | | playingSkillAnim = false; |
| | | |
| | | // 回收子技能轨道 |
| | | 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); |
| | | playingSkillAnim = false; |
| | | 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; |
| | |
| | | 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; |
| | | } |
| | |
| | | if (finalStarted && !finalEnded && currentFrame >= recoveryFrame) |
| | | { |
| | | finalEnded = true; |
| | | if (!isSubSkill) |
| | | |
| | | // 清理技能轨道 |
| | | if (skillTrack != null && activeSkillTracks.ContainsKey(trackIndex)) |
| | | { |
| | | playingSkillAnim = false; |
| | | 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(); |
| | |
| | | protected virtual void OnAnimationComplete(Spine.TrackEntry trackEntry) |
| | | { |
| | | if (trackEntry?.Animation?.Name == null) return; |
| | | |
| | | |
| | | string animName = trackEntry.Animation.Name.ToLower(); |
| | | |
| | | if (AttackMotionList.Contains(animName)) |
| | |
| | | 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() |
| | |
| | | |
| | | public virtual void Pause() |
| | | { |
| | | if (animState != null) animState.TimeScale = 0f; |
| | | if (skeletonAnim != null) skeletonAnim.timeScale = 0f; |
| | | |
| | | // 记录单次暂停开始时刻 |
| | | pauseStart = Time.time; |
| | | // 兼容旧字段(保留但不再用于累积逻辑) |
| | | pauseTime = Time.time; |
| | | } |
| | | |
| | | public virtual void Resume() |
| | | { |
| | | if (animState != null) animState.TimeScale = MotionTimeScale; |
| | | 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<int>(); |
| | | 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 (animState != null) animState.TimeScale = ratio; |
| | | if (skeletonAnim != null) skeletonAnim.timeScale = ratio; |
| | | } |
| | | |
| | | public void ShowIllusionShadow(bool isVisible) |
| | | public void ShowIllusionShadow(bool isVisible, Color color = default) |
| | | { |
| | | if (illusionShadow != null) |
| | | { |
| | | illusionShadow.SetSkeletonAnimation(skeletonAnim); |
| | | illusionShadow.Show(isVisible); |
| | | illusionShadow.Show(isVisible, color); |
| | | } |
| | | } |
| | | } |