| | |
| | | using System; |
| | | using System.Collections; |
| | | using System.Collections.Generic; |
| | | using UnityEngine; |
| | | using Spine.Unity; |
| | | using Cysharp.Threading.Tasks; |
| | | |
| | | /// <summary> |
| | | /// 角色动画基类,处理所有与动画相关的功能 |
| | | /// </summary> |
| | | public class MotionBase |
| | | { |
| | | public static float MotionTimeScale = 2f; |
| | | |
| | | public static float MotionTimeScale = 1f; |
| | | public static List<string> AttackMotionList = new List<string> |
| | | { |
| | | MotionName.attack.ToString(), |
| | | MotionName.angerSkill.ToString(), |
| | | MotionName.passiveSkill.ToString(), |
| | | MotionName.attack.ToString().ToLower(), |
| | | MotionName.angerSkill.ToString().ToLower(), |
| | | MotionName.passiveSkill.ToString().ToLower(), |
| | | }; |
| | | |
| | | private Dictionary<Spine.TrackEntry, Action> trackEntryCompleteDict = new Dictionary<Spine.TrackEntry, Action>(); |
| | | |
| | | // 动画事件 |
| | | private Dictionary<Spine.TrackEntry, Action> trackEntryCallbacks = new Dictionary<Spine.TrackEntry, Action>(); |
| | | public Action OnAttackAnimationComplete; |
| | | public Action OnHitAnimationComplete; |
| | | public Action<MotionName> onAnimationComplete; |
| | | private List<Action> runningActions = new List<Action>(); |
| | | |
| | | #region 组件引用 |
| | | |
| | | protected SkeletonGraphic skeletonGraphic; |
| | | protected Spine.AnimationState spineAnimationState; |
| | | public SkeletonAnimation skeletonAnim; |
| | | protected Spine.AnimationState animState; |
| | | protected Spine.Skeleton skeleton; |
| | | |
| | | #endregion |
| | | |
| | | #region 动画设置 |
| | | |
| | | // 动画混合时间 |
| | | protected float defaultMixDuration = 0f; |
| | | |
| | | #endregion |
| | | private Spine.TrackEntry currentTrack; |
| | | private SkeletonIllusionShadow illusionShadow; |
| | | private bool playingSkillAnim = false; |
| | | |
| | | private Spine.TrackEntry currentTrackEntry; |
| | | |
| | | #region 初始化方法 |
| | | |
| | | /// <summary> |
| | | /// 初始化动画组件 |
| | | /// </summary> |
| | | /// <param name="skeletonGraphic">骨骼动画组件</param> |
| | | public virtual void Init(SkeletonGraphic skeletonGraphic) |
| | | public virtual void Init(SkeletonAnimation skelAnim) |
| | | { |
| | | this.skeletonGraphic = skeletonGraphic; |
| | | |
| | | if (skeletonGraphic != null) |
| | | skeletonAnim = skelAnim; |
| | | if (skeletonAnim == null) |
| | | { |
| | | spineAnimationState = skeletonGraphic.AnimationState; |
| | | spineAnimationState.TimeScale = MotionTimeScale; |
| | | skeletonGraphic.timeScale = MotionTimeScale; |
| | | |
| | | skeleton = skeletonGraphic.Skeleton; |
| | | |
| | | // 设置动画混合时间 |
| | | if (spineAnimationState != null) |
| | | { |
| | | spineAnimationState.Data.DefaultMix = defaultMixDuration; |
| | | } |
| | | |
| | | // 播放默认动画 |
| | | PlayAnimation(MotionName.idle, true); |
| | | |
| | | // 设置动画事件监听 |
| | | SetupAnimationHandlers(); |
| | | BattleDebug.LogError("缺少SkeletonGraphic组件!"); |
| | | return; |
| | | } |
| | | else |
| | | { |
| | | Debug.LogError("缺少SkeletonGraphic组件!"); |
| | | } |
| | | |
| | | animState = skeletonAnim.AnimationState; |
| | | animState.TimeScale = MotionTimeScale; |
| | | skeletonAnim.timeScale = MotionTimeScale; |
| | | skeleton = skeletonAnim.Skeleton; |
| | | |
| | | if (animState != null) |
| | | animState.Data.DefaultMix = defaultMixDuration; |
| | | |
| | | PlayAnimation(MotionName.idle, true); |
| | | SetupAnimationHandlers(); |
| | | |
| | | |
| | | if (skelAnim.gameObject != null) |
| | | illusionShadow = skelAnim.gameObject.AddMissingComponent<SkeletonIllusionShadow>(); |
| | | } |
| | | |
| | | public virtual void Release() |
| | | { |
| | | if (spineAnimationState != null) |
| | | trackEntryCallbacks.Clear(); |
| | | if (animState != null) |
| | | { |
| | | spineAnimationState.Complete -= OnAnimationComplete; |
| | | spineAnimationState.ClearTracks(); |
| | | spineAnimationState = null; |
| | | animState.Complete -= OnAnimationComplete; |
| | | animState.ClearTracks(); |
| | | animState = null; |
| | | } |
| | | |
| | | skeletonGraphic = null; |
| | | skeletonAnim = null; |
| | | skeleton = null; |
| | | currentTrackEntry = null; |
| | | currentTrack = null; |
| | | playingSkillAnim = false; |
| | | } |
| | | |
| | | #endregion |
| | | |
| | | #region 动画控制 |
| | | |
| | | /// <summary> |
| | | /// 播放指定动画 |
| | | /// </summary> |
| | | /// <param name="motionName">动画枚举</param> |
| | | /// <param name="loop">是否循环</param> |
| | | /// <param name="_onComplete">动画播放完成回调</param> |
| | | /// <returns>动画轨道条目</returns> |
| | | public virtual Spine.TrackEntry PlayAnimation(MotionName motionName, bool loop, Action _onComplete = null) |
| | | public virtual Spine.TrackEntry PlayAnimation(MotionName motionName, bool loop, Action onComplete = null) |
| | | { |
| | | if (spineAnimationState == null) return null; |
| | | if (playingSkillAnim || animState == null) return null; |
| | | |
| | | // 如果当前动画未完成 |
| | | if (currentTrackEntry != null && !currentTrackEntry.IsComplete) |
| | | if (currentTrack != null && !currentTrack.IsComplete && trackEntryCallbacks.TryGetValue(currentTrack, out var prevCallback)) |
| | | { |
| | | if (trackEntryCompleteDict.TryGetValue(currentTrackEntry, out var __onComplete)) |
| | | { |
| | | __onComplete?.Invoke(); |
| | | trackEntryCompleteDict.Remove(currentTrackEntry); |
| | | } |
| | | currentTrackEntry = null; |
| | | trackEntryCallbacks.Remove(currentTrack); |
| | | prevCallback?.Invoke(); |
| | | currentTrack = null; |
| | | } |
| | | |
| | | // 直接使用 ToString() 而不是调用 GetAnimationName |
| | | currentTrackEntry = spineAnimationState.SetAnimation(0, motionName.ToString(), loop); |
| | | currentTrack = animState.SetAnimation(0, motionName.ToString(), loop); |
| | | if (onComplete != null && currentTrack != null) |
| | | trackEntryCallbacks[currentTrack] = onComplete; |
| | | |
| | | // 绑定回调 |
| | | if (_onComplete != null && currentTrackEntry != null) |
| | | { |
| | | trackEntryCompleteDict[currentTrackEntry] = _onComplete; |
| | | } |
| | | |
| | | return currentTrackEntry; |
| | | return currentTrack; |
| | | } |
| | | |
| | | public Spine.TrackEntry PlaySkillAnimation(SkillConfig skillConfig, Action onComplete = null, Action onBeginPhaseEnd = null, Action onActivePhaseEnd = null) |
| | | 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; |
| | | } |
| | | |
| | | return PlayAnimation(skillConfig.SkillMotionName, skillConfig.StartupFrames, skillConfig.ActiveFrames, skillConfig.LoopCount, |
| | | onComplete, onBeginPhaseEnd, onActivePhaseEnd); |
| | | 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); |
| | | } |
| | | |
| | | public virtual Spine.TrackEntry PlayAnimation( |
| | | string animationName, |
| | | int loopBeginFrame, |
| | | int loopEndFrame, |
| | | int loopTimes, |
| | | Action _onComplete = null, |
| | | Action onBeginPhaseEnd = null, // 前摇结束回调 |
| | | Action onActivePhaseEnd = null // 中摇结束回调 |
| | | ) |
| | | private Spine.TrackEntry ExecuteSkillAnim(SkillConfig skillConfig, SkillBase skillBase, Action onComplete, |
| | | Spine.Animation targetAnim, bool hasAnim, bool isSubSkill) |
| | | { |
| | | if (spineAnimationState == null || skeleton == null) return null; |
| | | int loopCount = skillConfig.LoopCount; |
| | | int[] activeFrames = skillConfig.ActiveFrames ?? new int[0]; |
| | | int frameCount = activeFrames.Length; |
| | | float recoveryFrame = skillConfig.RecoveryFrames; |
| | | |
| | | var anim = skeleton.Data.FindAnimation(animationName); |
| | | if (anim == null) return null; |
| | | |
| | | float fps = BattleConst.skillMotionFps; |
| | | float beginTime = loopBeginFrame / fps; |
| | | float endTime = loopEndFrame / fps; |
| | | |
| | | currentTrackEntry = spineAnimationState.SetAnimation(0, anim, false); |
| | | |
| | | int curLoop = 0; |
| | | bool finished = false; |
| | | bool beginPhaseTriggered = false; |
| | | |
| | | Spine.Unity.UpdateBonesDelegate updateLocalHandler = null; |
| | | updateLocalHandler = (ISkeletonAnimation animated) => |
| | | Spine.TrackEntry skillTrack = null; |
| | | if (hasAnim) |
| | | { |
| | | if (finished) return; |
| | | var entry = currentTrackEntry; |
| | | if (entry == null || entry.Animation != anim) |
| | | skillTrack = animState.SetAnimation(0, targetAnim, false); |
| | | currentTrack = 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; |
| | | |
| | | skillBase.OnSkillStart(); |
| | | |
| | | Action frameHandler = null; |
| | | frameHandler = () => |
| | | { |
| | | if (skillBase.IsFinished()) |
| | | { |
| | | skeletonGraphic.UpdateLocal -= updateLocalHandler; |
| | | playingSkillAnim = false; |
| | | RemoveAction(frameHandler); |
| | | return; |
| | | } |
| | | |
| | | // 前摇结束(只触发一次) |
| | | if (!beginPhaseTriggered && entry.TrackTime >= beginTime) |
| | | { |
| | | beginPhaseTriggered = true; |
| | | onBeginPhaseEnd?.Invoke(); |
| | | } |
| | | float currentFrame = 0f; |
| | | if (BattleConst.skillMotionFps > 0) |
| | | currentFrame = hasAnim ? (skillTrack.TrackTime * skillTrack.TimeScale * BattleConst.skillMotionFps) : ((Time.time - startTime) * MotionTimeScale * BattleConst.skillMotionFps); |
| | | |
| | | // 中摇结束(每次到endTime都触发) |
| | | if (entry.TrackTime >= endTime) |
| | | if (hasAnim) |
| | | { |
| | | onActivePhaseEnd?.Invoke(); |
| | | |
| | | curLoop++; |
| | | if (curLoop >= loopTimes) |
| | | if (currentTrack != skillTrack) |
| | | { |
| | | finished = true; |
| | | skeletonGraphic.UpdateLocal -= updateLocalHandler; |
| | | _onComplete?.Invoke(); |
| | | Debug.LogError("技能动画被打断,强制结束 " + skillConfig.SkillID); |
| | | skillBase.ForceFinished(); |
| | | RemoveAction(frameHandler); |
| | | playingSkillAnim = false; |
| | | return; |
| | | } |
| | | entry.TrackTime = beginTime; |
| | | beginPhaseTriggered = false; // 重置,下一轮前摇可再次触发 |
| | | |
| | | if (skillTrack.TrackTime == 0) failCount++; |
| | | if (failCount > 100) |
| | | { |
| | | Debug.LogError("技能动画播放失败,强制结束 " + skillConfig.SkillID); |
| | | skillBase.ForceFinished(); |
| | | RemoveAction(frameHandler); |
| | | 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 |
| | | startTime = Time.time - (skillConfig.StartupFrames / BattleConst.skillMotionFps); |
| | | } |
| | | 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 (!isSubSkill) |
| | | { |
| | | playingSkillAnim = false; |
| | | } |
| | | RemoveAction(frameHandler); |
| | | onComplete?.Invoke(); |
| | | skillBase.OnFinalFrameEnd(); |
| | | } |
| | | } |
| | | }; |
| | | skeletonGraphic.UpdateLocal += updateLocalHandler; |
| | | |
| | | if (_onComplete != null && currentTrackEntry != null) |
| | | 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) |
| | | { |
| | | trackEntryCompleteDict[currentTrackEntry] = _onComplete; |
| | | 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; |
| | | } |
| | | } |
| | | } |
| | | |
| | | return currentTrackEntry; |
| | | if (targetAnim == null) |
| | | Debug.LogError($"找不到动画: {animName}"); |
| | | |
| | | return targetAnim; |
| | | } |
| | | |
| | | |
| | | /// <summary> |
| | | /// 设置动画事件监听 |
| | | /// </summary> |
| | | |
| | | private void PlaySkillNoAnim(SkillConfig skillConfig, SkillBase skillBase, Action onComplete, bool isSubSkill) => |
| | | ExecuteSkillAnim(skillConfig, skillBase, onComplete, null, false, isSubSkill); |
| | | |
| | | protected virtual void SetupAnimationHandlers() |
| | | { |
| | | if (spineAnimationState == null) return; |
| | | |
| | | // 监听动画完成事件 |
| | | spineAnimationState.Complete += OnAnimationComplete; |
| | | if (animState != null) |
| | | animState.Complete += OnAnimationComplete; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 动画完成事件处理 |
| | | /// </summary> |
| | | protected virtual void OnAnimationComplete(Spine.TrackEntry trackEntry) |
| | | { |
| | | string animation = trackEntry.Animation.Name; |
| | | if (trackEntry?.Animation?.Name == null) return; |
| | | |
| | | string animName = trackEntry.Animation.Name.ToLower(); |
| | | |
| | | // 攻击动画完成后恢复到待机状态 |
| | | if (AttackMotionList.Contains(animation)) |
| | | if (AttackMotionList.Contains(animName)) |
| | | { |
| | | OnAttackAnimationComplete?.Invoke(); |
| | | PlayAnimation(MotionName.idle, true); |
| | | } |
| | | // 受伤动画完成后恢复到待机状态 可能触发多次 因为有多段攻击的存在 |
| | | else if (animation == MotionName.hit.ToString()) |
| | | else if (animName == MotionName.hit.ToString().ToLower()) |
| | | { |
| | | OnHitAnimationComplete?.Invoke(); |
| | | PlayAnimation(MotionName.idle, true); |
| | | } |
| | | onAnimationComplete?.Invoke((MotionName)Enum.Parse(typeof(MotionName), animation)); |
| | | |
| | | // 只调用本次TrackEntry的回调 |
| | | if (trackEntryCompleteDict.TryGetValue(trackEntry, out var cb)) |
| | | if (trackEntryCallbacks.TryGetValue(trackEntry, out var callback)) |
| | | { |
| | | cb?.Invoke(); |
| | | trackEntryCompleteDict.Remove(trackEntry); |
| | | trackEntryCallbacks.Remove(trackEntry); |
| | | callback?.Invoke(); |
| | | } |
| | | } |
| | | |
| | | |
| | | public void Test(string animationName, int beginFrame, int activeFrame, int endFrame, int activeFrameLoopCount) |
| | | { |
| | | // 要处理前摇beginFrame 后摇endFrame 中摇activeFrame |
| | | |
| | | // 中摇是有多次的activeFrameLoopCount |
| | | |
| | | var state = spineAnimationState; |
| | | var anim = skeleton.Data.FindAnimation(animationName); |
| | | |
| | | // 设定你要循环的区间(单位:秒) |
| | | float loopStart = 0.5f; |
| | | float loopEnd = 1.2f; |
| | | |
| | | // 播放动画 |
| | | state.SetAnimation(0, anim, true); |
| | | // state.GetCurrent(0).TrackTime = loopStart; |
| | | |
| | | int curFrame = 0; |
| | | |
| | | skeletonGraphic.UpdateLocal += (skeletonAnim) => |
| | | { |
| | | // if (curFrame == beginFrame) |
| | | // { |
| | | // OnBeginFrame?.Invoke(); |
| | | // } |
| | | // else if (curFrame == activeFrame) |
| | | // { |
| | | // OnActiveFrame?.Invoke(); |
| | | // } |
| | | // else if (curFrame == endFrame) |
| | | // { |
| | | // OnEndFrame?.Invoke(); |
| | | // } |
| | | // var trackEntry = state.GetCurrent(0); |
| | | // if (trackEntry != null && trackEntry.Animation == anim) |
| | | // { |
| | | // if (trackEntry.TrackTime > loopEnd) |
| | | // { |
| | | // // 回到loopStart,实现区间循环 |
| | | // trackEntry.TrackTime = loopStart; |
| | | // } |
| | | // } |
| | | }; |
| | | } |
| | | |
| | | |
| | | public virtual void Run() |
| | | { |
| | | for (int i = runningActions.Count - 1; i >= 0; i--) |
| | | runningActions[i]?.Invoke(); |
| | | |
| | | illusionShadow?.Run(); |
| | | } |
| | | |
| | | public virtual void Pause() |
| | | { |
| | | spineAnimationState.TimeScale = 0f; |
| | | skeletonGraphic.timeScale = 0f; |
| | | if (animState != null) animState.TimeScale = 0f; |
| | | if (skeletonAnim != null) skeletonAnim.timeScale = 0f; |
| | | } |
| | | |
| | | public virtual void Resume() |
| | | { |
| | | spineAnimationState.TimeScale = MotionTimeScale; |
| | | skeletonGraphic.timeScale = MotionTimeScale; |
| | | if (animState != null) animState.TimeScale = MotionTimeScale; |
| | | if (skeletonAnim != null) skeletonAnim.timeScale = MotionTimeScale; |
| | | } |
| | | |
| | | #endregion |
| | | |
| | | public void HaveRest() |
| | | { |
| | | trackEntryCallbacks.Clear(); |
| | | runningActions.Clear(); |
| | | 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) |
| | | { |
| | | if (illusionShadow != null) |
| | | { |
| | | illusionShadow.SetSkeletonAnimation(skeletonAnim); |
| | | illusionShadow.Show(isVisible); |
| | | } |
| | | } |
| | | } |