
1. 整体思路拆解Spine动画控制的底层逻辑做Unity项目遇到Spine动画第一反应都是“这不就调API播放吗”但真上手做复杂的战斗表现、剧情演出、UI动效之后你会发现播放只是最基础的一环。真正的难点在于动画和游戏逻辑怎么解耦、回调时机怎么卡准、动画生命周期怎么管理。先说Spine在Unity里的运行结构。Spine的动画文件从Spine Editor导出后会生成三个核心资产.skel或.json保存骨骼和动画数据.atlas记录图集信息纹理图集则是真正渲染用的贴图。在Unity中你用SkeletonAnimation组件来驱动它播放这个组件内部包含一个AnimationState对象和Skeleton对象。Skeleton管骨骼姿态AnimationState管动画的播放、混合、切换——注意Spine的动画播放并不是Unity的Animator而是一套独立的状态机。有个新手很容易混的概念Spine里的“动画”和Unity Animator里的“Clip”是两回事。Spine的每个动画本质上是一组骨骼关键帧数据播放时由AnimationState逐帧计算并输出到Skeleton。这意味着你可以在代码里同时控制多个轨道的动画——比如下半身走路、上半身攻击这种分轨控制在2D角色里非常常用。搞清楚这个结构之后播放、回调、停止这三件事就好理解了播放AnimationState.SetAnimation(trackIndex, animationName, loop)它替代当前轨道上的动画。回调AnimationState提供Start、End、Complete、Event等委托游戏逻辑在这些时机触发。停止ClearTrack清空轨道SetEmptyAnimation让骨骼回到绑定姿势两者有本质区别。我接触过的团队里十有七八都只在Update里判断AnimationState.GetCurrent(0).AnimationTime来卡点结果哪帧触发伤害判定永远差几毫秒改起来还到处是硬编码。这篇就把这套东西讲透从原理到实战一条龙。2. 播放和停止的核心API别只盯着SetAnimation2.1 SetAnimation与AddAnimation的正确用法SetAnimation是天上地下最常用的接口签名长这样TrackEntry trackEntry skeletonAnimation.AnimationState.SetAnimation( int trackIndex, string animationName, bool loop );它做的事情是把指定轨道上当前的动画立即替换成新动画返回一个TrackEntry对象。TrackEntry很重要它是你操作这个动画实例的句柄——改速度、改混合时间、注册回调全都要通过它。如果你想在播放完A动画后自动接B动画就有两种路线一种是自己监听A的Complete回调在回调里调用SetAnimation。另一种是用AddAnimation排队AnimationState state skeletonAnimation.AnimationState; // 立即播放idle循环 state.SetAnimation(0, idle, true); // 等当前动画播完之后再播attack不循环 state.AddAnimation(0, attack, false, 0f); // 再排一个walk循环 state.AddAnimation(0, walk, true, 0f);第三个参数delay为0时意思是“前一个动画一结束就立刻开始”如果给了正数则表示在上一动画播放完成前提前多少秒开始混合。实战中我经常用delay 0.1f之类的值做动画之间的过渡避免动作僵硬切换。还有一个细节AddAnimation返回的也是TrackEntry但它是队列里“未来才会播放”的条目。你对它设置timeScale时影响的是它真正播放之后的速度在它播放之前修改它的TrackTime是无效的。这个坑我踩过一次调了半天没反应后来发现操作的是队列里还没轮到的对象。2.2 停止动画的三种姿势与使用场景“停止”在Spine里不是一个单一操作要看你想让角色最终停在什么姿态ClearTrack(trackIndex)删除该轨道上的所有动画。调用之后骨骼会停留在当前骨骼姿态不会复位也不会继续更新。适合“角色死亡后保持死亡姿势”这种需求。ClearState()清空所有轨道的动画。效果等同于每一条轨道都调用ClearTrack适用于角色被销毁前或者切换出场状态时一次性清干净。SetEmptyAnimation(trackIndex, mixDuration)在指定轨道上播放一段“空动画”。空动画播放期间骨骼会渐渐回到绑定姿势T-Pose。注意mixDuration是混合时间设太短会瞬间跳回T-Pose看起来像抽搐设太长过渡会很慢。角色要进入待机前的缓冲动作时这个很有用。实际项目里我见过的错误操作是角色攻击结束开发者直接ClearTrack(0)结果角色保持着挥砍的姿势定在原地看起来像被时停了一样。因为你清空的是播放队列而不是把姿态归位。此时应该用SetEmptyAnimation或播放一个idle动画来接管。2.3 TrackIndex多轨道的实战意义很多新手不理解为什么要区分轨道直接永远用trackIndex 0。Spine设计多轨道的初衷是让身体的不同部位可以播放独立动画。典型场景角色一边跑下半身一边挥手上半身。你可以把跑步动画放在track 0挥手比如叫喊动画只做了上半身骨骼的动画放在track 1。两头同时播放互不干扰这就是Spine相较于传统序列帧的巨大优势。多轨道还有一个妙用用高轨道播放受击、闪现等临时表现结束后让轨道回到空动画角色自动回落到轨道0的基础状态。比如一个角色始终在播放待机loop受击时在track 1上SetAnimation(1, hit, false)受击动画播放完轨道1没有其他动画了角色自然回到轨道0的待机。这套逻辑非常干净不用在代码里手写状态切换。3. 回调函数实操动画与游戏逻辑的桥接3.1 回调种类与触发时机Spine的AnimationState提供了6个回调委托我把它们的触发时机和用途整理成了一张表回调触发时机典型用途Start某个动画条目开始播放时包括排队的动画轮到它时记录动画开始重置累积变量Interrupt某个正在播放的动画被其他动画打断时处理被中断的逻辑比如攻击被打断后重置连击计数End动画条目从队列/轨道上被移除时播放完移除或被打断移除都会触发清理该动画关联的临时状态Complete动画完整播放完一遍时只有非循环动画会触发连招衔接、动画播完后的收尾Event动画中包含Spine编辑器里定义的事件帧时播放到该帧触发出刀判定、音效、特效、飘字Dispose动画条目被释放时极少数情况会用到一般不需要处理需要注意两点Complete只在非循环动画播放完整一遍时触发End则无论动画是自然结束还是被强制打断只要它从播放序列里消失就会触发。如果你在End里做了资源释放而动画是被SetAnimation替换掉End一样会执行所以不要在End里写“动画播放完毕”这种业务逻辑它拦截的是“动画结束播放”而非“动画播完”。回调注册有两种方式。直接在AnimationState上注册全局委托skeletonAnimation.AnimationState.Complete OnAnimationComplete;也可以只针对某个TrackEntry注册TrackEntry entry state.SetAnimation(0, attack, false); entry.Complete entry { Debug.Log(attack 播完); };两者不冲突都会触发。我推荐的做法是全局委托处理框架级逻辑比如记录所有动画播放次数条目级委托处理具体业务的收尾功能。注意如果用全局委托且没有在OnDisable里减掉注册对象池里的角色销毁后委托仍会持有引用轻则内存泄漏重则在回调里访问已销毁对象直接抛异常。3.2 事件帧回调把判定做到帧级精确Event回调是Spine动画控制里最有价值、也最容易被错过的部分。Spine编辑器里可以在动画时间轴上添加Event事件每个事件带一个字符串名称还可以附带intValue、floatValue和stringValue这3个自定义数据。在Unity里state.Event (entry, spineEvent) { switch (spineEvent.Data.Name) { case hit: DamageTarget(spineEvent.FloatValue); break; case footstep: PlayFootstepSound(); break; } };这里最大的价值是事件帧和骨骼动画是绑定的美术在编辑动画时就能精确摆好“这帧出刀、这帧脚落地”程序不必再手工对帧。我做过一个项目攻击判定原来用TrackEntry.AnimationTime配合硬编码区间判断美术微调了攻击动画的节奏后我的判定窗口全错位。换成Event之后美术在Spine里把“击中帧”拖到想要的位置程序完全不用改代码。有个容易踩的坑Spine导入Unity时如果勾选了“Import”选项里的某个特殊选项或者工程里引用了旧版运行时Event可能出现不触发的情况。我的排查经验是——先确认Spine源文件的Event命名有没有拼错再用UnityEngine.Debug.Log打印spineEvent.Data.Name确认运行时到底收没收到最后再怀疑是导入配置的问题。3.3 两段式回调与ABC回调的区别网上搜“Spine 回调”经常会看到“两段式”和“ABC回调”的说法。简单说这是两种历史阶段的回调写法旧版Spine3.8及以前的运行时委托签名比较混乱Start、Complete等回调的事件参数不统一需要自己转型开发社区里总结出一套“两段式”写法先判断AnimationState的Event类型再从TrackEntry上取动画名分两段处理。新版Spine4.0及以后的运行时把委托类型抽象成了Spine.AnimationState.TrackEntryDelegate这种统一委托事件参数规范了一套写法通吃所有回调也就是社区里常说的“ABC回调”其实就是新API下按AnimationState统一处理回调。为什么要理解这个区别因为网上搜出来的老帖子和你自己下载的新运行时API往往对不上。你用Spine 4.1的包却对着3.8的教程写代码编译报错是小事最怕抄了过时的写法结果回调触发了但拿到的参数不符合预期。我的建议是直接去官方GitHub看对应版本的AnimationState.cs源码把委托定义翻一遍比任何二手教程都可靠。4. 实战一个完整的攻击连招控制Demo前面把概念讲完了接下来做一个完整的实战Demo。场景是一个2D角色待机时播放idle按攻击键播放attack1动画播放到“hit”事件帧时对目标造成伤害attack1播完自动连attack2attack2播完回到待机如果中途被怪打了一下强制停止当前连招播放hit动画hit播完回idle。先写代码挂在SkeletonAnimation同物体上using UnityEngine; using Spine.Unity; public class PlayerCombatController : MonoBehaviour { private SkeletonAnimation skeletonAnimation; private AnimationState state; [Header(动画名配置)] [SerializeField] private string idleAnim idle; [SerializeField] private string attack1Anim attack1; [SerializeField] private string attack2Anim attack2; [SerializeField] private string hitAnim hit; private int currentComboStep 0; private bool isAttacking false; private const int MainTrack 0; private void Awake() { skeletonAnimation GetComponentSkeletonAnimation(); state skeletonAnimation.AnimationState; // 全局事件回调处理动画中的事件帧 state.Event OnSpineEvent; // 监听当前攻击动画播完用于连招衔接 state.Complete OnAnimationComplete; } private void OnDestroy() { if (state ! null) { state.Event - OnSpineEvent; state.Complete - OnAnimationComplete; } } private void Start() { state.SetAnimation(MainTrack, idleAnim, true); } private void Update() { if (Input.GetKeyDown(KeyCode.J)) { TryAttack(); } if (Input.GetKeyDown(KeyCode.K)) { BeHit(); } } private void TryAttack() { if (isAttacking) { // 如果已经在攻击中不做处理也可以改成连招派生这里从简 return; } isAttacking true; currentComboStep 1; PlayAttackAnimation(attack1Anim); } private void PlayAttackAnimation(string animName) { // 设置动画并立刻在这条TrackEntry上注册Complete回调仅针对这次攻击 state.SetAnimation(MainTrack, animName, false); } private void OnSpineEvent(TrackEntry trackEntry, Spine.Event e) { if (e.Data.Name hit trackEntry.Animation.Name attack1Anim) { Debug.Log(attack1 命中帧触发对目标造成伤害); // 在这里触发伤害判定逻辑 DamageTarget(10); } else if (e.Data.Name hit trackEntry.Animation.Name attack2Anim) { Debug.Log(attack2 命中帧触发对目标造成伤害); DamageTarget(15); } } private void OnAnimationComplete(TrackEntry trackEntry) { // 注意Complete在SetAnimation替换动画时不会触发只有非循环动画自然播完才触发 if (!trackEntry.Loop) { if (trackEntry.Animation.Name attack1Anim) { // 连招attack1播完直接放attack2 state.SetAnimation(MainTrack, attack2Anim, false); } else if (trackEntry.Animation.Name attack2Anim) { // 连招收尾回到待机 isAttacking false; currentComboStep 0; state.SetAnimation(MainTrack, idleAnim, true); } } } private void BeHit() { // 受到攻击停止当前所有动画并播放受击 isAttacking false; currentComboStep 0; // 打断当前轨道动画播受伤动作 state.SetAnimation(MainTrack, hitAnim, false); // 在hit动画上注册一个Complete当它播完回idle state.GetCurrent(MainTrack).Complete entry { if (!entry.Loop entry.Animation.Name hitAnim) { state.SetAnimation(MainTrack, idleAnim, true); } }; } private void DamageTarget(int damage) { // 实际项目里找目标、计算伤害这里只演示 Debug.Log($造成 {damage} 点伤害); } }这段代码里藏着几个我在实际项目里反复踩坑后沉淀下来的写法习惯先看OnAnimationComplete。我在里面判断了trackEntry.Loop因为Complete回调的触发条件是非循环动画播完一遍。如果是循环动画Loop为true但正常不会触发Complete除非你在运行时改了入口参数的loop状态。判断Loop可以防止后续逻辑被意外触发。再看BeHit里给state.GetCurrent(MainTrack)注册Complete的写法。因为SetAnimation返回的是这趟动画的TrackEntry直接把它存成局部变量再注册也行但如果在其他代码路径里又调用了SetAnimation之前的TrackEntry就被替换了上面的Complete回调就不会触发。GetCurrent则始终取当前轨道上正在播放的条目更稳。这段代码运行起来的效果是按J后attack1挥出命中帧伤害触发attack1播完自动接attack2attack2播完回idle按K随时打断当前动作播受击动画hit播完自动回idle。整体就是一个麻雀虽小五脏俱全的状态控制闭环。5. 从播放到控制动画生命周期管理的进阶思路5.1 动画状态机封装上面Demo的写法能跑但放到更大的项目里你会发现到处是if trackEntry.Animation.Name xxx来判断动画状态代码越来越难维护。我的经验是做一层动画状态机封装把“当前在播什么动画”收敛到一处。一个简单可用的方案用枚举定义动画状态和外部需求对接内部映射到Spine动画名。public enum AnimState { Idle, Run, Attack, Hit, Dead } public class SpineAnimController { private SkeletonAnimation skeletonAnimation; private DictionaryAnimState, string animNameMap; private AnimState currentState; public void PlayState(AnimState nextState, bool loop false) { if (currentState nextState) return; string nextAnim animNameMap[nextState]; skeletonAnimation.AnimationState.SetAnimation(0, nextAnim, loop); currentState nextState; } public void StopState() { skeletonAnimation.AnimationState.SetEmptyAnimation(0, 0.15f); currentState AnimState.None; } }不要把Spine动画名散落在各个业务模块里不然美术改一个动画名你要全局搜索替换迟早改漏。集中映射之后美术改名只改一处。PlayState里的currentState判断能有效防止“同一个动画被反复SetAnimation导致瞬移闪跳”的问题。5.2 混合时间与动画过渡Spine的SetAnimation接口看不到混合参数但TrackEntry上有一个MixDuration属性它在动画切换时控制新旧动画的混合时长。默认值来自AnimationStateData的SetMix(fromAnim, toAnim, duration)配置。我通常会在初始化时做一套默认混合配置AnimationStateData stateData skeletonAnimation.SkeletonDataAsset.GetAnimationStateData(); stateData.SetMix(idle, run, 0.2f); stateData.SetMix(run, idle, 0.25f); stateData.SetMix(attack1, idle, 0.1f); stateData.SetMix(hit, idle, 0.1f);为什么混合时间这么重要因为Spine的骨骼动画新旧姿势直接切换会出“姿势瞬闪”或“肢体跳变”。混合时间给的越大过渡越平滑但动作响应越迟钝。格斗游戏里攻击帧要求手感硬朗混合时间就要压到0.05~0.1秒走路和跑步之间的切换可以宽松一点。5.3 对象池与动画复用的那点坑UI或战斗场景里的角色经常用对象池复用。对象池回收时如果不把Spine动画状态清干净会出现“复活”后角色保持死亡姿势或上一个动作的问题。我在第4节代码里写的OnDestroy反注册委托只是一个基础在对象池场景下还要注意回收时调用StopState()并让骨骼回到绑定姿势我建议state.ClearTrack(0); skeletonAnimation.Skeleton.SetToSetupPose();。SetToSetupPose()很关键它把骨骼重置到Spine编辑器的Setup Pose比播放empty动画更干净彻底。激活时重新注册委托要用OnEnable和OnDisable而不是Awake和OnDestroy否则对象池里的角色在禁用状态下旧委托仍然留在AnimationState上造成重复触发。大量角色都在同一帧播放动画时Update主线程的压力会很大。可以考虑把SkeletonAnimation的UpdateMode设置成AnimationState.Update手动驱动把动画更新统一放到FixedUpdate或干脆用SkeletonAnimation的定制更新接口去控制。6. 常见问题排查与踩坑经验6.1 Complete回调不触发或触发两次不触发常见原因有三个一是动画是循环动画Complete确实不会触发你该监听的是别的时机二是SetAnimation的第二个动画把前一个动画替换掉了前一个动画还没播完就被移除此时只有End没有Complete三是最容易忽视的Spine运行时版本问题旧版运行时Complete的参数类型不同导致你写的事件签名匹配不上。触发两次的情况全局委托和TrackEntry局部委托同时注册了同一个方法。排查时在回调里打印调用栈一眼就能看清是哪路逻辑重复处理。6.2 动画播放完姿态定格或闪回T-Pose回到T-Pose的原因通常是调用ClearTrack后没有提供后续动画骨骼没有数据驱动默认回落到Setup Pose。保持定格才是各种反馈里更常见的——ClearTrack后骨骼保持当前姿态但因为不再更新就像冻住了一样。如果想让动画在播放完的最后一帧上停住不要ClearTrack让动画自然播完并且不要触发任何切换。此时TrackEntry会停留在最后一帧角色自然定格。如果播完要回到一个中性姿态就调用SetEmptyAnimation(trackIndex, 0.2f)平滑过渡。6.3 事件帧没触发或触发位置不准先确认Spine源文件里Event确实加在了正确的骨骼动画时间轴上然后确认导出时Event数据被包含在.json或.skel里。如果一切正常但不触发检查Unity导入设置里的“Spine importer”版本以及代码注册的是AnimationState.Event而不是SkeletonAnimation的某个事件——两者不是一回事。触发位置不准多半不是Spine的锅而是你的判定逻辑里在事件回调中又做了异步或延迟处理。比如在Event里StartCoroutine等一帧再伤害那自然会有误差。伤害判定应该就近同步执行最多在事件回调里用Physics2D.Raycast做碰撞检测。6.4 播放不同动画时闪一下上一帧的画面这是混合时间导致的外观问题。MixDuration太短新旧动画切换时没有足够过渡视觉上会出现一瞬间的跳变。解决方式尽量用AnimationStateData.SetMix为特定动画对设置合理的过渡时长如果必须动态改通过TrackEntry.MixDuration设置。还有种情况是只改了MixDuration但没改TrackEntry的MixBlend。Spine的MixBlend有Replace、Add、SetupPose等模式对不同动画对MixBlend会影响混合结果。非特殊情况保持默认Replace即可。6.5 动画事件与回调顺序我遇到过业务方质疑“回调顺序不对”比如End出现在Start之前。这其实是正常的加入一个排在队列末尾的动画时它并不会立刻Start只有当它真正开始播时才Start。而被SetAnimation替换的动画会立刻End。所以当你在连招里预排队下一招时下一招的Start在上一招End之后才发生这中间可能隔了几帧甚至几秒不是Bug。处理这类问题时最好的工具就是在回调里打日志把TrackEntry.Animation.Name、回调类型、TrackTime、当前帧全部打出来。多打几次日志Spine事件流基本就摸清了。6.6 性能优化包围盒与合批热搜里有条“unity renderer的包围盒”在这个场景里同样适用。Spine的网格每帧都在变形Unity需要重新计算包围盒。如果角色在屏幕外还不暂停动画更新CPU白干活。新版运行时提供了SkeletonAnimation上直接设置UpdateMode的选项设成UpdateMode.FullUpdate、UpdateMode.AnimationOnly、UpdateMode.Nothing三级控制。合批方面Spine渲染使用MeshRenderer材质同一图集的多个角色可以共用材质但要注意Z轴排序和材质实例化。如果每个角色单独拷贝了材质DrawCall会飙升。用SkeletonAnimation自带的SkeletonRenderer里的meshGeneratorSettings可以控制材质生成方式。在实际项目里动态角色排序老老实实用SortingOrder别指望Unity自动合批。7. 性能优化与编辑器工具扩展7.1 按需更新UpdateMode选择SkeletonAnimation的UpdateMode有这几个应用场景UpdateMode.FullUpdate默认每帧更新动画和网格。适合屏幕上活跃战斗的角色。UpdateMode.AnimationOnly每帧更新动画数据但不重新生成网格。这种适合角色暂时不显示但需要保持动画时间同步的场景。比如一个角色在场景外待机一进场就要播放对应帧的动画这种模式可以避免重新播一段导致表现不一致。UpdateMode.Nothing完全不更新动画和网格。适合对象池里完全冻结的对象。UpdateMode.Cached部分版本有动画状态和网格都只在动画变化时更新一次适合长时间静止的UI模型。我实践中角色在Camera外就降级为AnimationOnly或Nothing进场恢复FullUpdate。注意切换UpdateMode后网格不会自动刷新恢复FullUpdate的第一帧有可能姿态跳跃需要配合SkeletonAnimation.LateUpdate调用强制刷新一次。7.2 编辑器辅助工具批量校验动画名项目大了动画名写错是常事。我写了个小工具放在Editor目录下一键检查场景里所有SkeletonAnimation的动画名是否存在于Spine的SkeletonData里#if UNITY_EDITOR using UnityEditor; using UnityEngine; using Spine.Unity; public class SpineAnimNameChecker : EditorWindow { [MenuItem(Tools/Spine/检查场景中的动画名)] public static void CheckAllSkeletonAnimations() { var allSkeleton FindObjectsOfTypeSkeletonAnimation(); foreach (var sa in allSkeleton) { var skeletonData sa.SkeletonDataAsset.GetSkeletonData(false); var animNames skeletonData.Animations; // 检查代码中引用的动画名是否符合 // 这里可以对接统一的动画配置表动态检查 string trackAnimName sa.AnimationName; if (string.IsNullOrEmpty(trackAnimName)) continue; if (!animNames.Any(a a.Name trackAnimName)) { Debug.LogError(${sa.gameObject.name} 动画名 {trackAnimName} 不存在, sa); } } } } #endif把动画名校验前置到编辑器阶段之后项目里因为动画名拼错导致的运行时空引用问题少了一大半。7.3 动画配置表驱动更进一步的方案是把所有角色动画做成ScriptableObject或者策划配置表。每个角色一个SpineAnimConfig资产里面是状态枚举到动画名的映射表。这样策划改动画名、加新动作都只需要改配置资源程序不用碰代码。动画名检查工具可以直接在配置项上校验合法性把“运行时才报错”变成“编辑时提示”。8. 一个容易被忽视的点SkeletonGraphic与UI场景如果你的Spine动画是要放到UI上比如活动界面、抽卡特效、对话框立绘千万别用SkeletonAnimation它的MeshRenderer在UI下会和Canvas渲染冲突。UI上的Spine动画要用SkeletonGraphic组件它挂在普通GameObject上配合CanvasRenderer工作接收RectTransform布局。SkeletonGraphic的API和SkeletonAnimation几乎一模一样也有AnimationState所以本文讲的播放、回调、停止逻辑在UI动画上完全复用。唯一注意的地方SkeletonGraphic的排序受Canvas.sortingOrder影响不要混用SortingOrder和Canvas层级UI排序要统一走Canvas体系。分辨率适配时SkeletonGraphic的缩放要放在RectTransform上不要改Skeleton.ScaleX/Y否则图集采样精度很容易出问题。半透明材质和UI的Mask结合使用需要检查SkeletonGraphic的材质是否支持Mask老版本默认不支持要手动指定UI Maskable材质。UI里的Spine动画播放完成回调比如特效播完销毁自身我习惯在Complete里判断非循环后直接PoolManager.Release(gameObject)或Destroy(gameObject)。特效用完全场不清理性能早晚出事。9. 个人经验总结做了几年Unity项目Spine动画这套控制体系从简单播放到复杂战斗演出踩了无数坑才理清思路。最重要的一条经验是动画回调不是你业务逻辑的主干只是一个信号源。不要在回调里写一大堆业务处理正确的姿势是把回调当作“这帧可以做某些事了”的通知具体做什么由上层决策。动画只管表现逻辑永远在别处。另一条关于协作的经验和美术配合时一定要提前约定好事帧、特效帧、音效帧的命名规范。比如统一以hit_、sfx_、fx_开头代码里按前缀分发美术加新事件程序不用改代码。我这边的最小约定是事件名带_前缀表示要传给程序的逻辑事件纯播放类的音效事件由美术自己在Spine里挂载音频播放组件程序不接管。最后说一个小技巧也是我最近在项目里用得比较多的Spine的AnimationState.TimeScale可以全局变速用它实现子弹时间、慢动作演出非常方便。但注意TimeScale同时影响所有轨道的所有动画如果你只想让某个动画加速直接设置对应TrackEntry.TimeScale。配合TrackEntry.TrackTime做定点驱动还能实现“战斗演出时动画暂停到某帧摆Pose”这种操作。Spine动画控制不难难的是把控制边界理清楚播放是输入回调是通知停止是清理它们之间的衔接理清了动画表现和游戏逻辑就不再互相打架了。