From fd26abdd88493fec1459481cd096ed9ed4413303 Mon Sep 17 00:00:00 2001 From: "harald.csaszar" Date: Mon, 3 Dec 2018 22:04:15 +0100 Subject: [PATCH] [unity] Fixed unity mecanim transition interruptions for SkeletonMecanim. Fixes #1208 --- .../Components/SkeletonAnimator.cs | 759 +++++++++++------- 1 file changed, 463 insertions(+), 296 deletions(-) diff --git a/spine-unity/Assets/spine-unity/Components/SkeletonAnimator.cs b/spine-unity/Assets/spine-unity/Components/SkeletonAnimator.cs index f6179d654..d9d5db4d4 100644 --- a/spine-unity/Assets/spine-unity/Components/SkeletonAnimator.cs +++ b/spine-unity/Assets/spine-unity/Components/SkeletonAnimator.cs @@ -1,296 +1,463 @@ -/****************************************************************************** - * Spine Runtimes Software License v2.5 - * - * Copyright (c) 2013-2016, Esoteric Software - * All rights reserved. - * - * You are granted a perpetual, non-exclusive, non-sublicensable, and - * non-transferable license to use, install, execute, and perform the Spine - * Runtimes software and derivative works solely for personal or internal - * use. Without the written permission of Esoteric Software (see Section 2 of - * the Spine Software License Agreement), you may not (a) modify, translate, - * adapt, or develop new applications using the Spine Runtimes or otherwise - * create derivative works or improvements of the Spine Runtimes or (b) remove, - * delete, alter, or obscure any trademarks or any copyright, trademark, patent, - * or other intellectual property or proprietary rights notices on or in the - * Software, including any copy thereof. Redistributions in binary or source - * form must include this license and terms. - * - * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "AS IS" AND ANY EXPRESS OR - * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO - * EVENT SHALL ESOTERIC SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF - * USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER - * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - *****************************************************************************/ - -using UnityEngine; -using System.Collections.Generic; - -namespace Spine.Unity { - [RequireComponent(typeof(Animator))] - public class SkeletonAnimator : SkeletonRenderer, ISkeletonAnimation { - - [SerializeField] protected MecanimTranslator translator; - public MecanimTranslator Translator { get { return translator; } } - - #region Bone Callbacks (ISkeletonAnimation) - protected event UpdateBonesDelegate _UpdateLocal; - protected event UpdateBonesDelegate _UpdateWorld; - protected event UpdateBonesDelegate _UpdateComplete; - - /// - /// Occurs after the animations are applied and before world space values are resolved. - /// Use this callback when you want to set bone local values. - public event UpdateBonesDelegate UpdateLocal { add { _UpdateLocal += value; } remove { _UpdateLocal -= value; } } - - /// - /// Occurs after the Skeleton's bone world space values are resolved (including all constraints). - /// Using this callback will cause the world space values to be solved an extra time. - /// Use this callback if want to use bone world space values, and also set bone local values. - public event UpdateBonesDelegate UpdateWorld { add { _UpdateWorld += value; } remove { _UpdateWorld -= value; } } - - /// - /// Occurs after the Skeleton's bone world space values are resolved (including all constraints). - /// Use this callback if you want to use bone world space values, but don't intend to modify bone local values. - /// This callback can also be used when setting world position and the bone matrix. - public event UpdateBonesDelegate UpdateComplete { add { _UpdateComplete += value; } remove { _UpdateComplete -= value; } } - #endregion - - public override void Initialize (bool overwrite) { - if (valid && !overwrite) return; - base.Initialize(overwrite); - if (!valid) return; - - if (translator == null) translator = new MecanimTranslator(); - translator.Initialize(GetComponent(), this.skeletonDataAsset); - } - - public void Update () { - if (!valid) return; - - #if UNITY_EDITOR - if (Application.isPlaying) { - translator.Apply(skeleton); - } else { - var translatorAnimator = translator.Animator; - if (translatorAnimator != null && translatorAnimator.isInitialized) - translator.Apply(skeleton); - } - #else - translator.Apply(skeleton); - #endif - - // UpdateWorldTransform and Bone Callbacks - { - if (_UpdateLocal != null) - _UpdateLocal(this); - - skeleton.UpdateWorldTransform(); - - if (_UpdateWorld != null) { - _UpdateWorld(this); - skeleton.UpdateWorldTransform(); - } - - if (_UpdateComplete != null) - _UpdateComplete(this); - } - } - - [System.Serializable] - public class MecanimTranslator { - #region Inspector - public bool autoReset = true; - public MixMode[] layerMixModes = new MixMode[0]; - #endregion - - public enum MixMode { AlwaysMix, MixNext, SpineStyle } - - readonly Dictionary animationTable = new Dictionary(IntEqualityComparer.Instance); - readonly Dictionary clipNameHashCodeTable = new Dictionary(AnimationClipEqualityComparer.Instance); - readonly List previousAnimations = new List(); - readonly List clipInfoCache = new List(); - readonly List nextClipInfoCache = new List(); - - Animator animator; - public Animator Animator { get { return this.animator; } } - - public void Initialize (Animator animator, SkeletonDataAsset skeletonDataAsset) { - this.animator = animator; - - previousAnimations.Clear(); - - animationTable.Clear(); - var data = skeletonDataAsset.GetSkeletonData(true); - foreach (var a in data.Animations) - animationTable.Add(a.Name.GetHashCode(), a); - - clipNameHashCodeTable.Clear(); - clipInfoCache.Clear(); - nextClipInfoCache.Clear(); - } - - public void Apply (Skeleton skeleton) { - if (layerMixModes.Length < animator.layerCount) - System.Array.Resize(ref layerMixModes, animator.layerCount); - - //skeleton.Update(Time.deltaTime); // Doesn't actually do anything, currently. (Spine 3.6). - - // Clear Previous - if (autoReset) { - var previousAnimations = this.previousAnimations; - for (int i = 0, n = previousAnimations.Count; i < n; i++) - previousAnimations[i].SetKeyedItemsToSetupPose(skeleton); - - previousAnimations.Clear(); - for (int layer = 0, n = animator.layerCount; layer < n; layer++) { - float layerWeight = (layer == 0) ? 1 : animator.GetLayerWeight(layer); // Animator.GetLayerWeight always returns 0 on the first layer. Should be interpreted as 1. - if (layerWeight <= 0) continue; - - AnimatorStateInfo nextStateInfo = animator.GetNextAnimatorStateInfo(layer); - - bool hasNext = nextStateInfo.fullPathHash != 0; - - int clipInfoCount, nextClipInfoCount; - IList clipInfo, nextClipInfo; - GetAnimatorClipInfos(layer, out clipInfoCount, out nextClipInfoCount, out clipInfo, out nextClipInfo); - - for (int c = 0; c < clipInfoCount; c++) { - var info = clipInfo[c]; - float weight = info.weight * layerWeight; if (weight == 0) continue; - previousAnimations.Add(GetAnimation(info.clip)); - } - - if (hasNext) { - for (int c = 0; c < nextClipInfoCount; c++) { - var info = nextClipInfo[c]; - float weight = info.weight * layerWeight; if (weight == 0) continue; - previousAnimations.Add(GetAnimation(info.clip)); - } - } - } - } - - // Apply - for (int layer = 0, n = animator.layerCount; layer < n; layer++) { - float layerWeight = (layer == 0) ? 1 : animator.GetLayerWeight(layer); // Animator.GetLayerWeight always returns 0 on the first layer. Should be interpreted as 1. - AnimatorStateInfo stateInfo = animator.GetCurrentAnimatorStateInfo(layer); - AnimatorStateInfo nextStateInfo = animator.GetNextAnimatorStateInfo(layer); - - bool hasNext = nextStateInfo.fullPathHash != 0; - - int clipInfoCount, nextClipInfoCount; - IList clipInfo, nextClipInfo; - GetAnimatorClipInfos(layer, out clipInfoCount, out nextClipInfoCount, out clipInfo, out nextClipInfo); - - MixMode mode = layerMixModes[layer]; - if (mode == MixMode.AlwaysMix) { - // Always use Mix instead of Applying the first non-zero weighted clip. - for (int c = 0; c < clipInfoCount; c++) { - var info = clipInfo[c]; float weight = info.weight * layerWeight; if (weight == 0) continue; - GetAnimation(info.clip).Apply(skeleton, 0, AnimationTime(stateInfo.normalizedTime, info.clip.length, stateInfo.loop, stateInfo.speed < 0), stateInfo.loop, null, weight, MixPose.Current, MixDirection.In); - } - if (hasNext) { - for (int c = 0; c < nextClipInfoCount; c++) { - var info = nextClipInfo[c]; float weight = info.weight * layerWeight; if (weight == 0) continue; - GetAnimation(info.clip).Apply(skeleton, 0, AnimationTime(nextStateInfo.normalizedTime , info.clip.length, nextStateInfo.speed < 0), nextStateInfo.loop, null, weight, MixPose.Current, MixDirection.In); - } - } - } else { // case MixNext || SpineStyle - // Apply first non-zero weighted clip - int c = 0; - for (; c < clipInfoCount; c++) { - var info = clipInfo[c]; float weight = info.weight * layerWeight; if (weight == 0) continue; - GetAnimation(info.clip).Apply(skeleton, 0, AnimationTime(stateInfo.normalizedTime, info.clip.length, stateInfo.loop, stateInfo.speed < 0), stateInfo.loop, null, 1f, MixPose.Current, MixDirection.In); - break; - } - // Mix the rest - for (; c < clipInfoCount; c++) { - var info = clipInfo[c]; float weight = info.weight * layerWeight; if (weight == 0) continue; - GetAnimation(info.clip).Apply(skeleton, 0, AnimationTime(stateInfo.normalizedTime, info.clip.length, stateInfo.loop, stateInfo.speed < 0), stateInfo.loop, null, weight, MixPose.Current, MixDirection.In); - } - - c = 0; - if (hasNext) { - // Apply next clip directly instead of mixing (ie: no crossfade, ignores mecanim transition weights) - if (mode == MixMode.SpineStyle) { - for (; c < nextClipInfoCount; c++) { - var info = nextClipInfo[c]; float weight = info.weight * layerWeight; if (weight == 0) continue; - GetAnimation(info.clip).Apply(skeleton, 0, AnimationTime(nextStateInfo.normalizedTime , info.clip.length, nextStateInfo.speed < 0), nextStateInfo.loop, null, 1f, MixPose.Current, MixDirection.In); - break; - } - } - // Mix the rest - for (; c < nextClipInfoCount; c++) { - var info = nextClipInfo[c]; float weight = info.weight * layerWeight; if (weight == 0) continue; - GetAnimation(info.clip).Apply(skeleton, 0, AnimationTime(nextStateInfo.normalizedTime , info.clip.length, nextStateInfo.speed < 0), nextStateInfo.loop, null, weight, MixPose.Current, MixDirection.In); - } - } - } - } - } - - static float AnimationTime (float normalizedTime, float clipLength, bool loop, bool reversed) { - if (reversed) - normalizedTime = (1-normalizedTime + (int)normalizedTime) + (int)normalizedTime; - float time = normalizedTime * clipLength; - if (loop) return time; - const float EndSnapEpsilon = 1f/30f; // Workaround for end-duration keys not being applied. - return (clipLength - time < EndSnapEpsilon) ? clipLength : time; // return a time snapped to clipLength; - } - - static float AnimationTime (float normalizedTime, float clipLength, bool reversed) { - if (reversed) - normalizedTime = (1-normalizedTime + (int)normalizedTime) + (int)normalizedTime; - - return normalizedTime * clipLength; - } - - void GetAnimatorClipInfos ( - int layer, - out int clipInfoCount, - out int nextClipInfoCount, - out IList clipInfo, - out IList nextClipInfo) { - clipInfoCount = animator.GetCurrentAnimatorClipInfoCount(layer); - nextClipInfoCount = animator.GetNextAnimatorClipInfoCount(layer); - if (clipInfoCache.Capacity < clipInfoCount) clipInfoCache.Capacity = clipInfoCount; - if (nextClipInfoCache.Capacity < nextClipInfoCount) nextClipInfoCache.Capacity = nextClipInfoCount; - animator.GetCurrentAnimatorClipInfo(layer, clipInfoCache); - animator.GetNextAnimatorClipInfo(layer, nextClipInfoCache); - - clipInfo = clipInfoCache; - nextClipInfo = nextClipInfoCache; - } - - Spine.Animation GetAnimation (AnimationClip clip) { - int clipNameHashCode; - if (!clipNameHashCodeTable.TryGetValue(clip, out clipNameHashCode)) { - clipNameHashCode = clip.name.GetHashCode(); - clipNameHashCodeTable.Add(clip, clipNameHashCode); - } - Spine.Animation animation; - animationTable.TryGetValue(clipNameHashCode, out animation); - return animation; - } - - class AnimationClipEqualityComparer : IEqualityComparer { - internal static readonly IEqualityComparer Instance = new AnimationClipEqualityComparer(); - public bool Equals (AnimationClip x, AnimationClip y) { return x.GetInstanceID() == y.GetInstanceID(); } - public int GetHashCode (AnimationClip o) { return o.GetInstanceID(); } - } - - class IntEqualityComparer : IEqualityComparer { - internal static readonly IEqualityComparer Instance = new IntEqualityComparer(); - public bool Equals (int x, int y) { return x == y; } - public int GetHashCode(int o) { return o; } - } - } - - } -} +/****************************************************************************** + * Spine Runtimes Software License v2.5 + * + * Copyright (c) 2013-2016, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable, and + * non-transferable license to use, install, execute, and perform the Spine + * Runtimes software and derivative works solely for personal or internal + * use. Without the written permission of Esoteric Software (see Section 2 of + * the Spine Software License Agreement), you may not (a) modify, translate, + * adapt, or develop new applications using the Spine Runtimes or otherwise + * create derivative works or improvements of the Spine Runtimes or (b) remove, + * delete, alter, or obscure any trademarks or any copyright, trademark, patent, + * or other intellectual property or proprietary rights notices on or in the + * Software, including any copy thereof. Redistributions in binary or source + * form must include this license and terms. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL ESOTERIC SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF + * USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER + * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using UnityEngine; +using System.Collections.Generic; + +namespace Spine.Unity { + [RequireComponent(typeof(Animator))] + public class SkeletonAnimator : SkeletonRenderer, ISkeletonAnimation { + + [SerializeField] protected MecanimTranslator translator; + public MecanimTranslator Translator { get { return translator; } } + + #region Bone Callbacks (ISkeletonAnimation) + protected event UpdateBonesDelegate _UpdateLocal; + protected event UpdateBonesDelegate _UpdateWorld; + protected event UpdateBonesDelegate _UpdateComplete; + + /// + /// Occurs after the animations are applied and before world space values are resolved. + /// Use this callback when you want to set bone local values. + public event UpdateBonesDelegate UpdateLocal { add { _UpdateLocal += value; } remove { _UpdateLocal -= value; } } + + /// + /// Occurs after the Skeleton's bone world space values are resolved (including all constraints). + /// Using this callback will cause the world space values to be solved an extra time. + /// Use this callback if want to use bone world space values, and also set bone local values. + public event UpdateBonesDelegate UpdateWorld { add { _UpdateWorld += value; } remove { _UpdateWorld -= value; } } + + /// + /// Occurs after the Skeleton's bone world space values are resolved (including all constraints). + /// Use this callback if you want to use bone world space values, but don't intend to modify bone local values. + /// This callback can also be used when setting world position and the bone matrix. + public event UpdateBonesDelegate UpdateComplete { add { _UpdateComplete += value; } remove { _UpdateComplete -= value; } } + #endregion + + public override void Initialize (bool overwrite) { + if (valid && !overwrite) return; + base.Initialize(overwrite); + if (!valid) return; + + if (translator == null) translator = new MecanimTranslator(); + translator.Initialize(GetComponent(), this.skeletonDataAsset); + } + + public void Update () { + if (!valid) return; + + #if UNITY_EDITOR + if (Application.isPlaying) { + translator.Apply(skeleton); + } else { + var translatorAnimator = translator.Animator; + if (translatorAnimator != null && translatorAnimator.isInitialized) + translator.Apply(skeleton); + } + #else + translator.Apply(skeleton); + #endif + + // UpdateWorldTransform and Bone Callbacks + { + if (_UpdateLocal != null) + _UpdateLocal(this); + + skeleton.UpdateWorldTransform(); + + if (_UpdateWorld != null) { + _UpdateWorld(this); + skeleton.UpdateWorldTransform(); + } + + if (_UpdateComplete != null) + _UpdateComplete(this); + } + } + + [System.Serializable] + public class MecanimTranslator { + #region Inspector + public bool autoReset = true; + public MixMode[] layerMixModes = new MixMode[0]; + #endregion + + public enum MixMode { AlwaysMix, MixNext, SpineStyle } + + readonly Dictionary animationTable = new Dictionary(IntEqualityComparer.Instance); + readonly Dictionary clipNameHashCodeTable = new Dictionary(AnimationClipEqualityComparer.Instance); + readonly List previousAnimations = new List(); + + protected class ClipInfos { + public bool isInterruptionActive = false; + public bool isLastFrameOfInterruption = false; + + public int clipInfoCount = 0; + public int nextClipInfoCount = 0; + public int interruptingClipInfoCount = 0; + public readonly List clipInfos = new List(); + public readonly List nextClipInfos = new List(); + public readonly List interruptingClipInfos = new List(); + + public AnimatorStateInfo stateInfo; + public AnimatorStateInfo nextStateInfo; + public AnimatorStateInfo interruptingStateInfo; + + public float interruptingClipTimeAddition = 0; + } + protected ClipInfos[] layerClipInfos = new ClipInfos[0]; + + Animator animator; + public Animator Animator { get { return this.animator; } } + + public void Initialize(Animator animator, SkeletonDataAsset skeletonDataAsset) { + this.animator = animator; + + previousAnimations.Clear(); + + animationTable.Clear(); + var data = skeletonDataAsset.GetSkeletonData(true); + foreach (var a in data.Animations) + animationTable.Add(a.Name.GetHashCode(), a); + + clipNameHashCodeTable.Clear(); + ClearClipInfosForLayers(); + } + + public void Apply(Skeleton skeleton) { + if (layerMixModes.Length < animator.layerCount) + System.Array.Resize(ref layerMixModes, animator.layerCount); + + InitClipInfosForLayers(); + for (int layer = 0, n = animator.layerCount; layer < n; layer++) { + GetStateUpdatesFromAnimator(layer); + } + + //skeleton.Update(Time.deltaTime); // Doesn't actually do anything, currently. (Spine 3.6). + + // Clear Previous + if (autoReset) { + var previousAnimations = this.previousAnimations; + for (int i = 0, n = previousAnimations.Count; i < n; i++) + previousAnimations[i].SetKeyedItemsToSetupPose(skeleton); + + previousAnimations.Clear(); + for (int layer = 0, n = animator.layerCount; layer < n; layer++) { + float layerWeight = (layer == 0) ? 1 : animator.GetLayerWeight(layer); // Animator.GetLayerWeight always returns 0 on the first layer. Should be interpreted as 1. + if (layerWeight <= 0) continue; + + AnimatorStateInfo nextStateInfo = animator.GetNextAnimatorStateInfo(layer); + + bool hasNext = nextStateInfo.fullPathHash != 0; + + int clipInfoCount, nextClipInfoCount, interruptingClipInfoCount; + IList clipInfo, nextClipInfo, interruptingClipInfo; + bool isInterruptionActive, shallInterpolateWeightTo1; + GetAnimatorClipInfos(layer, out isInterruptionActive, out clipInfoCount, out nextClipInfoCount, out interruptingClipInfoCount, + out clipInfo, out nextClipInfo, out interruptingClipInfo, out shallInterpolateWeightTo1); + + for (int c = 0; c < clipInfoCount; c++) { + var info = clipInfo[c]; + float weight = info.weight * layerWeight; if (weight == 0) continue; + previousAnimations.Add(GetAnimation(info.clip)); + } + + if (hasNext) { + for (int c = 0; c < nextClipInfoCount; c++) { + var info = nextClipInfo[c]; + float weight = info.weight * layerWeight; if (weight == 0) continue; + previousAnimations.Add(GetAnimation(info.clip)); + } + } + + if (isInterruptionActive) { + for (int c = 0; c < interruptingClipInfoCount; c++) + { + var info = interruptingClipInfo[c]; + float clipWeight = shallInterpolateWeightTo1 ? (info.weight + 1.0f) * 0.5f : info.weight; + float weight = clipWeight * layerWeight; if (weight == 0) continue; + previousAnimations.Add(GetAnimation(info.clip)); + } + } + } + } + + // Apply + for (int layer = 0, n = animator.layerCount; layer < n; layer++) { + float layerWeight = (layer == 0) ? 1 : animator.GetLayerWeight(layer); // Animator.GetLayerWeight always returns 0 on the first layer. Should be interpreted as 1. + + bool isInterruptionActive; + AnimatorStateInfo stateInfo; + AnimatorStateInfo nextStateInfo; + AnimatorStateInfo interruptingStateInfo; + float interruptingClipTimeAddition; + GetAnimatorStateInfos(layer, out isInterruptionActive, out stateInfo, out nextStateInfo, out interruptingStateInfo, out interruptingClipTimeAddition); + + bool hasNext = nextStateInfo.fullPathHash != 0; + + int clipInfoCount, nextClipInfoCount, interruptingClipInfoCount; + IList clipInfo, nextClipInfo, interruptingClipInfo; + bool shallInterpolateWeightTo1; + GetAnimatorClipInfos(layer, out isInterruptionActive, out clipInfoCount, out nextClipInfoCount, out interruptingClipInfoCount, + out clipInfo, out nextClipInfo, out interruptingClipInfo, out shallInterpolateWeightTo1); + + MixMode mode = layerMixModes[layer]; + if (mode == MixMode.AlwaysMix) { + // Always use Mix instead of Applying the first non-zero weighted clip. + for (int c = 0; c < clipInfoCount; c++) { + var info = clipInfo[c]; float weight = info.weight * layerWeight; if (weight == 0) continue; + GetAnimation(info.clip).Apply(skeleton, 0, AnimationTime(stateInfo.normalizedTime, info.clip.length, stateInfo.loop, stateInfo.speed < 0), stateInfo.loop, null, weight, MixPose.Current, MixDirection.In); + } + if (hasNext) { + for (int c = 0; c < nextClipInfoCount; c++) { + var info = nextClipInfo[c]; float weight = info.weight * layerWeight; if (weight == 0) continue; + GetAnimation(info.clip).Apply(skeleton, 0, AnimationTime(nextStateInfo.normalizedTime, info.clip.length, nextStateInfo.speed < 0), nextStateInfo.loop, null, weight, MixPose.Current, MixDirection.In); + } + } + if (isInterruptionActive) { + for (int c = 0; c < interruptingClipInfoCount; c++) + { + var info = interruptingClipInfo[c]; + float clipWeight = shallInterpolateWeightTo1 ? (info.weight + 1.0f) * 0.5f : info.weight; + float weight = clipWeight * layerWeight; if (weight == 0) continue; + GetAnimation(info.clip).Apply(skeleton, 0, AnimationTime(interruptingStateInfo.normalizedTime + interruptingClipTimeAddition, info.clip.length, interruptingStateInfo.speed < 0), + interruptingStateInfo.loop, null, weight, MixPose.Current, MixDirection.In); + } + } + } else { // case MixNext || SpineStyle + // Apply first non-zero weighted clip + int c = 0; + for (; c < clipInfoCount; c++) { + var info = clipInfo[c]; float weight = info.weight * layerWeight; if (weight == 0) continue; + GetAnimation(info.clip).Apply(skeleton, 0, AnimationTime(stateInfo.normalizedTime, info.clip.length, stateInfo.loop, stateInfo.speed < 0), stateInfo.loop, null, 1f, MixPose.Current, MixDirection.In); + break; + } + // Mix the rest + for (; c < clipInfoCount; c++) { + var info = clipInfo[c]; float weight = info.weight * layerWeight; if (weight == 0) continue; + GetAnimation(info.clip).Apply(skeleton, 0, AnimationTime(stateInfo.normalizedTime, info.clip.length, stateInfo.loop, stateInfo.speed < 0), stateInfo.loop, null, weight, MixPose.Current, MixDirection.In); + } + + c = 0; + if (hasNext) { + // Apply next clip directly instead of mixing (ie: no crossfade, ignores mecanim transition weights) + if (mode == MixMode.SpineStyle) { + for (; c < nextClipInfoCount; c++) { + var info = nextClipInfo[c]; float weight = info.weight * layerWeight; if (weight == 0) continue; + GetAnimation(info.clip).Apply(skeleton, 0, AnimationTime(nextStateInfo.normalizedTime , info.clip.length, nextStateInfo.speed < 0), nextStateInfo.loop, null, 1f, MixPose.Current, MixDirection.In); + break; + } + } + // Mix the rest + for (; c < nextClipInfoCount; c++) { + var info = nextClipInfo[c]; float weight = info.weight * layerWeight; if (weight == 0) continue; + GetAnimation(info.clip).Apply(skeleton, 0, AnimationTime(nextStateInfo.normalizedTime , info.clip.length, nextStateInfo.speed < 0), nextStateInfo.loop, null, weight, MixPose.Current, MixDirection.In); + } + } + + c = 0; + if (isInterruptionActive) { + // Apply next clip directly instead of mixing (ie: no crossfade, ignores mecanim transition weights) + if (mode == MixMode.SpineStyle) { + for (; c < interruptingClipInfoCount; c++) { + var info = interruptingClipInfo[c]; float clipWeight = shallInterpolateWeightTo1 ? (info.weight + 1.0f) * 0.5f : info.weight; + float weight = clipWeight * layerWeight; if (weight == 0) continue; + GetAnimation(info.clip).Apply(skeleton, 0, AnimationTime(interruptingStateInfo.normalizedTime + interruptingClipTimeAddition, info.clip.length, interruptingStateInfo.speed < 0), interruptingStateInfo.loop, null, 1f, MixPose.Current, MixDirection.In); + break; + } + } + // Mix the rest + for (; c < interruptingClipInfoCount; c++) { + var info = interruptingClipInfo[c]; float clipWeight = shallInterpolateWeightTo1 ? (info.weight + 1.0f) * 0.5f : info.weight; + float weight = clipWeight * layerWeight; if (weight == 0) continue; + GetAnimation(info.clip).Apply(skeleton, 0, AnimationTime(interruptingStateInfo.normalizedTime + interruptingClipTimeAddition, info.clip.length, interruptingStateInfo.speed < 0), interruptingStateInfo.loop, null, weight, MixPose.Current, MixDirection.In); + } + } + } + } + } + + static float AnimationTime(float normalizedTime, float clipLength, bool loop, bool reversed) { + if (reversed) + normalizedTime = (1 - normalizedTime + (int)normalizedTime) + (int)normalizedTime; + float time = normalizedTime * clipLength; + if (loop) return time; + const float EndSnapEpsilon = 1f / 30f; // Workaround for end-duration keys not being applied. + return (clipLength - time < EndSnapEpsilon) ? clipLength : time; // return a time snapped to clipLength; + } + + static float AnimationTime(float normalizedTime, float clipLength, bool reversed) { + if (reversed) + normalizedTime = (1 - normalizedTime + (int)normalizedTime) + (int)normalizedTime; + + return normalizedTime * clipLength; + } + + void InitClipInfosForLayers() { + if (layerClipInfos.Length < animator.layerCount) { + System.Array.Resize(ref layerClipInfos, animator.layerCount); + for (int layer = 0, n = animator.layerCount; layer < n; ++layer) { + if (layerClipInfos[layer] == null) + layerClipInfos[layer] = new ClipInfos(); + } + } + } + + void ClearClipInfosForLayers() { + for (int layer = 0, n = layerClipInfos.Length; layer < n; ++layer) { + if (layerClipInfos[layer] == null) + layerClipInfos[layer] = new ClipInfos(); + else { + layerClipInfos[layer].isInterruptionActive = false; + layerClipInfos[layer].isLastFrameOfInterruption = false; + layerClipInfos[layer].clipInfos.Clear(); + layerClipInfos[layer].nextClipInfos.Clear(); + layerClipInfos[layer].interruptingClipInfos.Clear(); + } + } + } + + void GetStateUpdatesFromAnimator(int layer) { + + var layerInfos = layerClipInfos[layer]; + int clipInfoCount = animator.GetCurrentAnimatorClipInfoCount(layer); + int nextClipInfoCount = animator.GetNextAnimatorClipInfoCount(layer); + + var clipInfos = layerInfos.clipInfos; + var nextClipInfos = layerInfos.nextClipInfos; + var interruptingClipInfos = layerInfos.interruptingClipInfos; + + layerInfos.isInterruptionActive = (clipInfoCount == 0 && nextClipInfoCount == 0); + + // Note: during interruption, GetCurrentAnimatorClipInfoCount and GetNextAnimatorClipInfoCount + // are returning 0 in calls above. Therefore we keep previous clipInfos and nextClipInfos + // until the interruption is over. + if (layerInfos.isInterruptionActive) { + + // Note: The last frame of a transition interruption + // will have fullPathHash set to 0, therefore we have to use previous + // frame's infos about interruption clips and correct some values + // accordingly (normalizedTime and weight). + var interruptingStateInfo = animator.GetNextAnimatorStateInfo(layer); + layerInfos.isLastFrameOfInterruption = interruptingStateInfo.fullPathHash == 0; + if (!layerInfos.isLastFrameOfInterruption) { + layerInfos.interruptingClipInfoCount = interruptingClipInfos.Count; + + animator.GetNextAnimatorClipInfo(layer, interruptingClipInfos); + float oldTime = layerInfos.interruptingStateInfo.normalizedTime; + float newTime = interruptingStateInfo.normalizedTime; + layerInfos.interruptingClipTimeAddition = newTime - oldTime; + layerInfos.interruptingStateInfo = interruptingStateInfo; + } + } + else { + layerInfos.clipInfoCount = clipInfoCount; + layerInfos.nextClipInfoCount = nextClipInfoCount; + layerInfos.interruptingClipInfoCount = 0; + layerInfos.isLastFrameOfInterruption = false; + + if (clipInfos.Capacity < clipInfoCount) clipInfos.Capacity = clipInfoCount; + if (nextClipInfos.Capacity < nextClipInfoCount) nextClipInfos.Capacity = nextClipInfoCount; + + animator.GetCurrentAnimatorClipInfo(layer, clipInfos); + animator.GetNextAnimatorClipInfo(layer, nextClipInfos); + + layerInfos.stateInfo = animator.GetCurrentAnimatorStateInfo(layer); + layerInfos.nextStateInfo = animator.GetNextAnimatorStateInfo(layer); + } + } + + void GetAnimatorClipInfos ( + int layer, + out bool isInterruptionActive, + out int clipInfoCount, + out int nextClipInfoCount, + out int interruptingClipInfoCount, + out IList clipInfo, + out IList nextClipInfo, + out IList interruptingClipInfo, + out bool shallInterpolateWeightTo1) { + + var layerInfos = layerClipInfos[layer]; + isInterruptionActive = layerInfos.isInterruptionActive; + + clipInfoCount = layerInfos.clipInfoCount; + nextClipInfoCount = layerInfos.nextClipInfoCount; + interruptingClipInfoCount = layerInfos.interruptingClipInfoCount; + + clipInfo = layerInfos.clipInfos; + nextClipInfo = layerInfos.nextClipInfos; + interruptingClipInfo = isInterruptionActive ? layerInfos.interruptingClipInfos : null; + shallInterpolateWeightTo1 = layerInfos.isLastFrameOfInterruption; + } + + void GetAnimatorStateInfos( + int layer, + out bool isInterruptionActive, + out AnimatorStateInfo stateInfo, + out AnimatorStateInfo nextStateInfo, + out AnimatorStateInfo interruptingStateInfo, + out float interruptingClipTimeAddition) { + + var layerInfos = layerClipInfos[layer]; + isInterruptionActive = layerInfos.isInterruptionActive; + + stateInfo = layerInfos.stateInfo; + nextStateInfo = layerInfos.nextStateInfo; + interruptingStateInfo = layerInfos.interruptingStateInfo; + interruptingClipTimeAddition = layerInfos.isLastFrameOfInterruption ? layerInfos.interruptingClipTimeAddition : 0; + } + + Spine.Animation GetAnimation (AnimationClip clip) { + int clipNameHashCode; + if (!clipNameHashCodeTable.TryGetValue(clip, out clipNameHashCode)) { + clipNameHashCode = clip.name.GetHashCode(); + clipNameHashCodeTable.Add(clip, clipNameHashCode); + } + Spine.Animation animation; + animationTable.TryGetValue(clipNameHashCode, out animation); + return animation; + } + + class AnimationClipEqualityComparer : IEqualityComparer { + internal static readonly IEqualityComparer Instance = new AnimationClipEqualityComparer(); + public bool Equals (AnimationClip x, AnimationClip y) { return x.GetInstanceID() == y.GetInstanceID(); } + public int GetHashCode (AnimationClip o) { return o.GetInstanceID(); } + } + + class IntEqualityComparer : IEqualityComparer { + internal static readonly IEqualityComparer Instance = new IntEqualityComparer(); + public bool Equals (int x, int y) { return x == y; } + public int GetHashCode(int o) { return o; } + } + } + + } +}