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 = 1f;
|
|
public static List<string> AttackMotionList = new List<string>
|
{
|
MotionName.attack.ToString().ToLower(),
|
MotionName.angerSkill.ToString().ToLower(),
|
MotionName.passiveSkill.ToString().ToLower(),
|
};
|
|
private Dictionary<Spine.TrackEntry, Action> trackEntryCompleteDict = new Dictionary<Spine.TrackEntry, Action>();
|
|
// 动画事件
|
public Action OnAttackAnimationComplete;
|
public Action OnHitAnimationComplete;
|
public Action<string> onAnimationComplete;
|
|
#region 组件引用
|
|
protected SkeletonGraphic skeletonGraphic;
|
protected Spine.AnimationState spineAnimationState;
|
protected Spine.Skeleton skeleton;
|
|
#endregion
|
|
#region 动画设置
|
|
// 动画混合时间
|
protected float defaultMixDuration = 0f;
|
|
#endregion
|
|
private Spine.TrackEntry currentTrackEntry;
|
|
#region 初始化方法
|
|
/// <summary>
|
/// 初始化动画组件
|
/// </summary>
|
/// <param name="skeletonGraphic">骨骼动画组件</param>
|
public virtual void Init(SkeletonGraphic skeletonGraphic)
|
{
|
this.skeletonGraphic = skeletonGraphic;
|
|
if (skeletonGraphic != 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();
|
}
|
else
|
{
|
BattleDebug.LogError("缺少SkeletonGraphic组件!");
|
}
|
|
|
}
|
|
public virtual void Release()
|
{
|
if (spineAnimationState != null)
|
{
|
spineAnimationState.Complete -= OnAnimationComplete;
|
spineAnimationState.ClearTracks();
|
spineAnimationState = null;
|
}
|
|
skeletonGraphic = null;
|
skeleton = null;
|
currentTrackEntry = null;
|
}
|
|
#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)
|
{
|
if (spineAnimationState == null) return null;
|
|
// 如果当前动画未完成
|
if (currentTrackEntry != null && !currentTrackEntry.IsComplete)
|
{
|
if (trackEntryCompleteDict.TryGetValue(currentTrackEntry, out var __onComplete))
|
{
|
__onComplete?.Invoke();
|
trackEntryCompleteDict.Remove(currentTrackEntry);
|
}
|
currentTrackEntry = null;
|
}
|
|
// 直接使用 ToString() 而不是调用 GetAnimationName
|
currentTrackEntry = spineAnimationState.SetAnimation(0, motionName.ToString(), loop);
|
|
// 绑定回调
|
if (_onComplete != null && currentTrackEntry != null)
|
{
|
trackEntryCompleteDict[currentTrackEntry] = _onComplete;
|
}
|
|
return currentTrackEntry;
|
}
|
|
public Spine.TrackEntry PlaySkillAnimation(SkillConfig skillConfig, SkillBase skillBase, Action _onComplete = null)
|
{
|
// 参数校验
|
if (skillConfig == null)
|
{
|
BattleDebug.LogError("技能配置为空,无法播放技能动画");
|
return null;
|
}
|
if (spineAnimationState == null || skeleton == null)
|
{
|
BattleDebug.LogError("SkeletonGraphic或AnimationState未初始化,无法播放技能动画");
|
return null;
|
}
|
|
|
Spine.Animation anim = skeleton.Data.FindAnimation(skillConfig.SkillMotionName);
|
|
if (null == anim)
|
{
|
for (int i = 0; i < skeleton.Data.Animations.Count; i++)
|
{
|
var skeletonAnim = skeleton.Data.Animations.Items[i];
|
if (skeletonAnim.Name.ToLower() == skillConfig.SkillMotionName.ToLower())
|
{
|
anim = skeletonAnim;
|
// 找到动画
|
break;
|
}
|
}
|
}
|
|
// 获取动画
|
if (anim == null)
|
{
|
BattleDebug.LogError($"找不到动画: {skillConfig.SkillMotionName}");
|
_onComplete?.Invoke();
|
return null;
|
}
|
|
// 关键帧参数
|
float fps = BattleConst.skillMotionFps;
|
float middleBeginTime = skillConfig.StartupFrames / fps;
|
int loopCount = skillConfig.LoopCount;
|
int[] activeFrames = skillConfig.ActiveFrames;
|
int activeFrameCount = activeFrames.Length;
|
float recoveryFrameTime = skillConfig.RecoveryFrames / fps;
|
|
// 播放动画
|
var skillTrackEntry = spineAnimationState.SetAnimation(0, anim, false);
|
currentTrackEntry = skillTrackEntry;
|
|
// 事件状态
|
int curLoop = 0;
|
bool isFinish = false;
|
bool beginPhaseTriggered = false;
|
bool finalFrameStarted = false;
|
bool finalFrameEnded = false;
|
bool middleFrameStarted = false;
|
bool[] triggeredActiveFrame = new bool[activeFrameCount];
|
|
// 技能开始
|
skillBase.OnSkillStart();
|
|
// 动画帧更新处理
|
Spine.Unity.UpdateBonesDelegate updateLocalHandler = null;
|
updateLocalHandler = (ISkeletonAnimation animated) =>
|
{
|
if (isFinish) return;
|
float trackTime = skillTrackEntry.TrackTime;
|
|
// 前摇结束(只触发一次)
|
if (!beginPhaseTriggered && trackTime >= middleBeginTime && curLoop == 0)
|
{
|
beginPhaseTriggered = true;
|
skillBase.OnStartSkillFrameEnd();
|
}
|
|
// 中摇开始(每轮loop的开始,只触发一次)
|
if (!middleFrameStarted && trackTime >= middleBeginTime && curLoop <= loopCount)
|
{
|
middleFrameStarted = true;
|
skillBase.OnMiddleFrameStart(curLoop);
|
}
|
|
// 多段攻击帧触发
|
for (int hitIndex = 0; hitIndex < activeFrameCount; hitIndex++)
|
{
|
float activeFrameTime = activeFrames[hitIndex] / fps;
|
if (!triggeredActiveFrame[hitIndex] && trackTime >= activeFrameTime)
|
{
|
skillBase.OnMiddleFrameEnd(curLoop, hitIndex);
|
triggeredActiveFrame[hitIndex] = true;
|
}
|
}
|
|
// 判断是否所有activeFrame都已触发,准备进入下一轮loop
|
bool allTriggered = true;
|
for (int i = 0; i < activeFrameCount; i++)
|
{
|
if (!triggeredActiveFrame[i])
|
{
|
allTriggered = false;
|
break;
|
}
|
}
|
|
// 进入下一轮loop
|
if (allTriggered && curLoop < loopCount)
|
{
|
curLoop++;
|
Array.Clear(triggeredActiveFrame, 0, activeFrameCount);
|
middleFrameStarted = false;
|
|
if (curLoop < loopCount)
|
{
|
// 重新设置到第一次的中摇时间
|
skillTrackEntry.TrackTime = middleBeginTime;
|
beginPhaseTriggered = false;
|
}
|
else
|
{
|
finalFrameStarted = false;
|
finalFrameEnded = false;
|
// 收尾阶段由后续逻辑处理
|
}
|
}
|
|
// 收尾阶段:OnFinalFrameStart 和 OnFinalFrameEnd
|
if (curLoop >= loopCount)
|
{
|
if (!finalFrameStarted && trackTime >= recoveryFrameTime)
|
{
|
finalFrameStarted = true;
|
skillBase.OnFinalFrameStart();
|
}
|
if (finalFrameStarted && !finalFrameEnded && trackTime >= recoveryFrameTime)
|
{
|
finalFrameEnded = true;
|
skillBase.OnFinalFrameEnd();
|
skeletonGraphic.UpdateLocal -= updateLocalHandler;
|
isFinish = true;
|
}
|
}
|
};
|
|
if (_onComplete != null && currentTrackEntry != null)
|
{
|
trackEntryCompleteDict[currentTrackEntry] = _onComplete;
|
}
|
|
skeletonGraphic.UpdateLocal += updateLocalHandler;
|
return skillTrackEntry;
|
}
|
|
|
/// <summary>
|
/// 设置动画事件监听
|
/// </summary>
|
protected virtual void SetupAnimationHandlers()
|
{
|
if (spineAnimationState == null) return;
|
|
// 监听动画完成事件
|
spineAnimationState.Complete += OnAnimationComplete;
|
}
|
|
/// <summary>
|
/// 动画完成事件处理
|
/// </summary>
|
protected virtual void OnAnimationComplete(Spine.TrackEntry trackEntry)
|
{
|
string animation = trackEntry.Animation.Name.ToLower();
|
|
// 攻击动画完成后恢复到待机状态
|
if (AttackMotionList.Contains(animation))
|
{
|
OnAttackAnimationComplete?.Invoke();
|
PlayAnimation(MotionName.idle, true);
|
}
|
// 受伤动画完成后恢复到待机状态 可能触发多次 因为有多段攻击的存在
|
else if (animation == MotionName.hit.ToString().ToLower())
|
{
|
OnHitAnimationComplete?.Invoke();
|
PlayAnimation(MotionName.idle, true);
|
}
|
|
onAnimationComplete?.Invoke(animation);
|
|
// 只调用本次TrackEntry的回调
|
if (trackEntryCompleteDict.TryGetValue(trackEntry, out var cb))
|
{
|
cb?.Invoke();
|
trackEntryCompleteDict.Remove(trackEntry);
|
}
|
}
|
|
|
|
public virtual void Run()
|
{
|
|
}
|
|
public virtual void Pause()
|
{
|
spineAnimationState.TimeScale = 0f;
|
skeletonGraphic.timeScale = 0f;
|
}
|
|
public virtual void Resume()
|
{
|
spineAnimationState.TimeScale = MotionTimeScale;
|
skeletonGraphic.timeScale = MotionTimeScale;
|
}
|
|
#endregion
|
|
}
|