/****************************************************************************** * Spine Runtimes License Agreement * Last updated April 5, 2025. Replaces all prior versions. * * Copyright (c) 2013-2026, Esoteric Software LLC * * Integration of the Spine Runtimes into software or otherwise creating * derivative works of the Spine Runtimes is permitted under the terms and * conditions of Section 2 of the Spine Editor License Agreement: * http://esotericsoftware.com/spine-editor-license * * Otherwise, it is permitted to integrate the Spine Runtimes into software * or otherwise create derivative works of the Spine Runtimes (collectively, * "Products"), provided that each user of the Products must obtain their own * Spine Editor license and redistribution of the Products in any form must * include this license and copyright notice. * * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "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 LLC 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 * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *****************************************************************************/ #if !SPINE_AUTO_UPGRADE_COMPONENTS_OFF #define AUTO_UPGRADE_TO_43_COMPONENTS #endif #if UNITY_2017_1_OR_NEWER #define BUILT_IN_SPRITE_MASK_COMPONENT #endif #if UNITY_6000_3_OR_NEWER #define USES_ENTITY_ID #endif using System.Collections.Generic; using UnityEngine; using UnityEngine.Serialization; #if UNITY_EDITOR using UnityEditor.Animations; #endif namespace Spine.Unity { #if NEW_PREFAB_SYSTEM [ExecuteAlways] #else [ExecuteInEditMode] #endif [RequireComponent(typeof(Animator))] [HelpURL("https://esotericsoftware.com/spine-unity-main-components#SkeletonMecanim-Component")] public class SkeletonMecanim : SkeletonAnimationBase, IUpgradable { public SkeletonMecanim.MecanimTranslator translator; public MecanimTranslator Translator { get { return translator; } } public UnityEngine.Animator AnimatorComponent { get { return this.GetComponent(); } } public override bool IsValid { get { return skeletonRenderer != null && skeletonRenderer.IsValid && translator != null && translator.Animator && translator.Animator.isInitialized; } } public override void InitializeAnimationComponent () { base.InitializeAnimationComponent(); if (!skeletonRenderer.IsValid) return; if (translator == null) translator = new MecanimTranslator(); translator.Initialize(this.AnimatorComponent, skeletonRenderer.SkeletonDataAsset); } protected override void UpdateAnimationStatus (float deltaTime) { // Note: main animation status is updated by Mecanim Animator component skeleton.Update(deltaTime); } public override void MainThreadBeforeUpdateInternal () { base.MainThreadBeforeUpdateInternal(); translator.GatherAnimatorState(); } protected override void ApplyStateToSkeleton (bool calledFromMainThread) { #if UNITY_EDITOR if (calledFromMainThread) EditorRebindAnimator(); if (ApplicationIsPlaying || !calledFromMainThread) { translator.Apply(skeletonRenderer.Skeleton); } else { Animator translatorAnimator = translator.Animator; if (translatorAnimator != null && translatorAnimator.isInitialized && translatorAnimator.isActiveAndEnabled && translatorAnimator.runtimeAnimatorController != null) { // Note: Rebind is required to prevent warning "Animator is not playing an AnimatorController" with prefabs translatorAnimator.Rebind(); translator.Apply(skeletonRenderer.Skeleton); } } #else translator.Apply(skeletonRenderer.Skeleton); #endif } #if UNITY_EDITOR private void EditorRebindAnimator () { Animator translatorAnimator = translator.Animator; if (translatorAnimator != null && !translatorAnimator.isInitialized) translatorAnimator.Rebind(); } #endif #region Transfer of Deprecated Fields #if UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS // compatibility layer between 4.1 and 4.2, automatically transfer serialized attributes. public override void UpgradeTo43 () { if (!Application.isPlaying && !wasDeprecatedTransferred) { UpgradeTo43Components(); TransferDeprecatedFields(); InitializeAnimationComponent(); } } protected void UpgradeTo43Components () { if (gameObject.GetComponent() == null && gameObject.GetComponent() == null) { gameObject.AddComponent(); Debug.Log(string.Format("{0}: Auto-migrated old SkeletonMecanim component to split SkeletonMecanim + SkeletonRenderer components.", gameObject.name), gameObject); EditorBridge.RequestMarkDirty(gameObject); } } /// Transfer of former base class SkeletonRenderer parameters. protected void TransferDeprecatedFields () { wasDeprecatedTransferred = true; SkeletonRenderer skeletonRenderer = gameObject.GetComponent(); if (skeletonRenderer == null) return; skeletonRenderer.skeletonDataAsset = this.skeletonDataAssetDeprecated; skeletonRenderer.initialSkinName = this.initialSkinNameDeprecated; skeletonRenderer.EditorSkipSkinSync = this.editorSkipSkinSyncDeprecated; skeletonRenderer.initialFlipX = this.initialFlipXDeprecated; skeletonRenderer.initialFlipY = this.initialFlipYDeprecated; skeletonRenderer.UpdateMode = this.updateModeDeprecated; skeletonRenderer.updateWhenInvisible = this.updateWhenInvisibleDeprecated; skeletonRenderer.separatorSlotNames = this.separatorSlotNamesDeprecated; skeletonRenderer.MeshSettings.zSpacing = this.zSpacingDeprecated; skeletonRenderer.MeshSettings.useClipping = this.useClippingDeprecated; skeletonRenderer.MeshSettings.immutableTriangles = this.immutableTrianglesDeprecated; skeletonRenderer.MeshSettings.pmaVertexColors = this.pmaVertexColorsDeprecated; skeletonRenderer.MeshSettings.tintBlack = this.tintBlackDeprecated; skeletonRenderer.MeshSettings.addNormals = this.addNormalsDeprecated; skeletonRenderer.MeshSettings.calculateTangents = this.calculateTangentsDeprecated; skeletonRenderer.clearStateOnDisable = this.clearStateOnDisableDeprecated; skeletonRenderer.singleSubmesh = this.singleSubmeshDeprecated; skeletonRenderer.MaskInteraction = this.maskInteractionDeprecated; translator.TransferDeprecatedFields(); } [SerializeField] protected bool wasDeprecatedTransferred = false; // SkeletonRenderer former base class parameters [FormerlySerializedAs("skeletonDataAsset")] [SerializeField] private SkeletonDataAsset skeletonDataAssetDeprecated; [FormerlySerializedAs("initialSkinName")] [SpineSkin(defaultAsEmptyString: true)] [SerializeField] private string initialSkinNameDeprecated; [FormerlySerializedAs("editorSkipSkinSync")] [SerializeField] private bool editorSkipSkinSyncDeprecated = false; [FormerlySerializedAs("initialFlipX")] [SerializeField] private bool initialFlipXDeprecated = false; [FormerlySerializedAs("initialFlipY")] [SerializeField] private bool initialFlipYDeprecated = false; [FormerlySerializedAs("updateMode")] [SerializeField] private UpdateMode updateModeDeprecated = UpdateMode.FullUpdate; [FormerlySerializedAs("updateWhenInvisible")] [SerializeField] private UpdateMode updateWhenInvisibleDeprecated = UpdateMode.FullUpdate; [UnityEngine.Serialization.FormerlySerializedAs("submeshSeparators"), UnityEngine.Serialization.FormerlySerializedAs("separatorSlotNames")] [SerializeField] private string[] separatorSlotNamesDeprecated = new string[0]; [FormerlySerializedAs("zSpacing")] [SerializeField] private float zSpacingDeprecated = 0f; [FormerlySerializedAs("useClipping")] [SerializeField] private bool useClippingDeprecated = true; [FormerlySerializedAs("immutableTriangles")] [SerializeField] private bool immutableTrianglesDeprecated = false; [FormerlySerializedAs("pmaVertexColors")] [SerializeField] private bool pmaVertexColorsDeprecated = true; [FormerlySerializedAs("clearStateOnDisable")] [SerializeField] private bool clearStateOnDisableDeprecated = false; [FormerlySerializedAs("tintBlack")] [SerializeField] private bool tintBlackDeprecated = false; [FormerlySerializedAs("singleSubmesh")] [SerializeField] private bool singleSubmeshDeprecated = false; [FormerlySerializedAs("calculateNormals"), FormerlySerializedAs("addNormals")] [SerializeField] private bool addNormalsDeprecated = false; [FormerlySerializedAs("calculateTangents")] [SerializeField] private bool calculateTangentsDeprecated = false; #if BUILT_IN_SPRITE_MASK_COMPONENT [FormerlySerializedAs("maskInteraction")] [SerializeField] private SpriteMaskInteraction maskInteractionDeprecated = SpriteMaskInteraction.None; #endif // BUILT_IN_SPRITE_MASK_COMPONENT #endif // UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS #endregion [System.Serializable] public class MecanimTranslator { const float WeightEpsilon = 0.0001f; #region Inspector public bool autoReset = true; public bool useCustomMixMode = true; public MixMode[] layerMixModes = new MixMode[0]; public bool[] layerIsAdditive = new bool[0]; // Migration from removed MixBlend [FormerlySerializedAs("layerBlendModes")] [SerializeField] private int[] layerBlendModesDeprecated = new int[0]; #endregion public delegate void OnClipAppliedDelegate (Spine.Animation clip, int layerIndex, float weight, float time, float lastTime, bool playsBackward); protected event OnClipAppliedDelegate _OnClipApplied; public event OnClipAppliedDelegate OnClipApplied { add { _OnClipApplied += value; } remove { _OnClipApplied -= value; } } public enum MixMode { AlwaysMix, MixNext, Hard, Match } readonly Dictionary animationTable = new Dictionary(IntEqualityComparer.Instance); readonly Dictionary clipNameHashCodeTable = new Dictionary(AnimationClipEqualityComparer.Instance); readonly List previousAnimations = new List(); protected struct ClipInfo { public Spine.Animation animation; public float weight; public float length; public bool isLooping; } 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 float[] clipResolvedWeights = new float[0]; public float[] nextClipResolvedWeights = new float[0]; public float[] interruptingClipResolvedWeights = new float[0]; public AnimatorStateInfo stateInfo; public AnimatorStateInfo nextStateInfo; public AnimatorStateInfo interruptingStateInfo; public float interruptingClipTimeAddition = 0; public float layerWeight = 0; } protected ClipInfos[] layerClipInfos = new ClipInfos[0]; protected int layerCount = 0; Animator animator; public Animator Animator { get { return this.animator; } } public int MecanimLayerCount { get { if (!animator) return 0; if (!animator.isInitialized || !animator.isActiveAndEnabled || animator.runtimeAnimatorController == null) return 0; return animator.layerCount; } } public string[] MecanimLayerNames { get { if (!animator) return new string[0]; string[] layerNames = new string[animator.layerCount]; for (int i = 0; i < animator.layerCount; ++i) { layerNames[i] = animator.GetLayerName(i); } return layerNames; } } public void Initialize (Animator animator, SkeletonDataAsset skeletonDataAsset) { this.animator = animator; previousAnimations.Clear(); animationTable.Clear(); SkeletonData data = skeletonDataAsset.GetSkeletonData(true); foreach (Animation a in data.Animations) animationTable.Add(a.Name.GetHashCode(), a); clipNameHashCodeTable.Clear(); ClearClipInfosForLayers(); } private bool ApplyAnimation (Skeleton skeleton, ClipInfo info, AnimatorStateInfo stateInfo, int layerIndex, float layerWeight, bool layerIsAdditive, bool fromSetup, bool useCustomClipWeight = false, float customClipWeight = 1.0f) { float weight = info.weight * layerWeight; if (weight < WeightEpsilon) return false; if (info.animation == null) return false; float time = AnimationTime(stateInfo.normalizedTime, info.length, info.isLooping, stateInfo.speed < 0); weight = useCustomClipWeight ? layerWeight * customClipWeight : weight; info.animation.Apply(skeleton, 0, time, info.isLooping, null, weight, fromSetup, layerIsAdditive, false, false); if (_OnClipApplied != null) OnClipAppliedCallback(info.animation, stateInfo, layerIndex, time, info.isLooping, weight); return true; } private bool ApplyInterruptionAnimation (Skeleton skeleton, bool interpolateWeightTo1, ClipInfo info, AnimatorStateInfo stateInfo, int layerIndex, float layerWeight, bool layerIsAdditive, float interruptingClipTimeAddition, bool useCustomClipWeight = false, float customClipWeight = 1.0f) { float clipWeight = interpolateWeightTo1 ? (info.weight + 1.0f) * 0.5f : info.weight; float weight = clipWeight * layerWeight; if (weight < WeightEpsilon) return false; if (info.animation == null) return false; float time = AnimationTime(stateInfo.normalizedTime + interruptingClipTimeAddition, info.length, stateInfo.speed < 0); weight = useCustomClipWeight ? layerWeight * customClipWeight : weight; bool fromSetup = false; info.animation.Apply(skeleton, 0, time, info.isLooping, null, weight, fromSetup, layerIsAdditive, false, false); if (_OnClipApplied != null) { OnClipAppliedCallback(info.animation, stateInfo, layerIndex, time, info.isLooping, weight); } return true; } private void OnClipAppliedCallback (Spine.Animation animation, AnimatorStateInfo stateInfo, int layerIndex, float time, bool isLooping, float weight) { float speedFactor = stateInfo.speedMultiplier * stateInfo.speed; float lastTime = time - (Time.deltaTime * speedFactor); if (isLooping && animation.Duration != 0) { time %= animation.Duration; lastTime %= animation.Duration; } _OnClipApplied(animation, layerIndex, weight, time, lastTime, speedFactor < 0); } public void GatherAnimatorState () { layerCount = animator.layerCount; MigrateLayerBlendModes(); #if UNITY_EDITOR GetLayerBlendModes(); #endif if (layerIsAdditive.Length < layerCount) System.Array.Resize(ref layerIsAdditive, layerCount); if (layerMixModes.Length < layerCount) { int oldSize = layerMixModes.Length; System.Array.Resize(ref layerMixModes, layerCount); for (int layer = oldSize; layer < layerCount; ++layer) { layerMixModes[layer] = layerIsAdditive[layer] ? MixMode.AlwaysMix : MixMode.MixNext; } } InitClipInfosForLayers(); for (int layer = 0, n = layerCount; layer < n; layer++) { GetStateUpdatesFromAnimator(layer); } } public void Apply (Skeleton skeleton) { // Clear Previous if (autoReset) { List previousAnimations = this.previousAnimations; for (int i = 0, n = previousAnimations.Count; i < n; i++) previousAnimations[i].Apply(skeleton, 0, 0, false, null, 0, true, false, true, false); // SetKeyedItemsToSetupPose previousAnimations.Clear(); for (int layer = 0, n = layerCount; layer < n; layer++) { ClipInfos layerInfos = layerClipInfos[layer]; float layerWeight = layerInfos.layerWeight; if (layerWeight <= 0) continue; AnimatorStateInfo nextStateInfo = layerInfos.nextStateInfo; 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++) { ClipInfo info = clipInfo[c]; float weight = info.weight * layerWeight; if (weight < WeightEpsilon) continue; if (info.animation != null) previousAnimations.Add(info.animation); } if (hasNext) { for (int c = 0; c < nextClipInfoCount; c++) { ClipInfo info = nextClipInfo[c]; float weight = info.weight * layerWeight; if (weight < WeightEpsilon) continue; if (info.animation != null) previousAnimations.Add(info.animation); } } if (isInterruptionActive) { for (int c = 0; c < interruptingClipInfoCount; c++) { ClipInfo info = interruptingClipInfo[c]; float clipWeight = shallInterpolateWeightTo1 ? (info.weight + 1.0f) * 0.5f : info.weight; float weight = clipWeight * layerWeight; if (weight < WeightEpsilon) continue; if (info.animation != null) previousAnimations.Add(info.animation); } } } } // Apply int appliedCount = 0; for (int layer = 0, n = layerCount; layer < n; layer++) { ClipInfos layerInfos = layerClipInfos[layer]; float layerWeight = layerInfos.layerWeight; 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 interpolateWeightTo1; GetAnimatorClipInfos(layer, out isInterruptionActive, out clipInfoCount, out nextClipInfoCount, out interruptingClipInfoCount, out clipInfo, out nextClipInfo, out interruptingClipInfo, out interpolateWeightTo1); bool add = layerIsAdditive[layer]; MixMode mode = GetMixMode(layer, add); if (mode == MixMode.AlwaysMix) { // Always use Mix instead of Applying the first non-zero weighted clip. for (int c = 0; c < clipInfoCount; c++) { ApplyAnimation(skeleton, clipInfo[c], stateInfo, layer, layerWeight, add, appliedCount++ == 0); } if (hasNext) { for (int c = 0; c < nextClipInfoCount; c++) { ApplyAnimation(skeleton, nextClipInfo[c], nextStateInfo, layer, layerWeight, add, appliedCount++ == 0); } } if (isInterruptionActive) { for (int c = 0; c < interruptingClipInfoCount; c++) { appliedCount++; ApplyInterruptionAnimation(skeleton, interpolateWeightTo1, interruptingClipInfo[c], interruptingStateInfo, layer, layerWeight, add, interruptingClipTimeAddition); } } } else if (mode == MixMode.Match) { // Calculate matching Spine lerp(lerp(A, B, w2), C, w3) weights // from Unity's absolute weights A*W1 + B*W2 + C*W3. MatchWeights(layerClipInfos[layer], hasNext, isInterruptionActive, clipInfoCount, nextClipInfoCount, interruptingClipInfoCount, clipInfo, nextClipInfo, interruptingClipInfo); float[] customWeights = layerClipInfos[layer].clipResolvedWeights; for (int c = 0; c < clipInfoCount; c++) { ApplyAnimation(skeleton, clipInfo[c], stateInfo, layer, layerWeight, add, appliedCount++ == 0, true, customWeights[c]); } if (hasNext) { customWeights = layerClipInfos[layer].nextClipResolvedWeights; for (int c = 0; c < nextClipInfoCount; c++) { ApplyAnimation(skeleton, nextClipInfo[c], nextStateInfo, layer, layerWeight, add, appliedCount++ == 0, true, customWeights[c]); } } if (isInterruptionActive) { customWeights = layerClipInfos[layer].interruptingClipResolvedWeights; for (int c = 0; c < interruptingClipInfoCount; c++) { appliedCount++; ApplyInterruptionAnimation(skeleton, interpolateWeightTo1, interruptingClipInfo[c], interruptingStateInfo, layer, layerWeight, add, interruptingClipTimeAddition, true, customWeights[c]); } } } else { // case MixNext || Hard // Apply first non-zero weighted clip int c = 0; for (; c < clipInfoCount; c++) { if (!ApplyAnimation(skeleton, clipInfo[c], stateInfo, layer, layerWeight, add, appliedCount++ == 0, true, 1.0f)) continue; ++c; break; } // Mix the rest for (; c < clipInfoCount; c++) { ApplyAnimation(skeleton, clipInfo[c], stateInfo, layer, layerWeight, add, appliedCount++ == 0); } c = 0; if (hasNext) { // Apply next clip directly instead of mixing (ie: no crossfade, ignores mecanim transition weights) if (mode == MixMode.Hard) { for (; c < nextClipInfoCount; c++) { if (!ApplyAnimation(skeleton, nextClipInfo[c], nextStateInfo, layer, layerWeight, add, appliedCount++ == 0, true, 1.0f)) continue; ++c; break; } } // Mix the rest for (; c < nextClipInfoCount; c++) { if (!ApplyAnimation(skeleton, nextClipInfo[c], nextStateInfo, layer, layerWeight, add, appliedCount++ == 0)) continue; } } c = 0; if (isInterruptionActive) { // Apply next clip directly instead of mixing (ie: no crossfade, ignores mecanim transition weights) if (mode == MixMode.Hard) { for (; c < interruptingClipInfoCount; c++) { appliedCount++; if (ApplyInterruptionAnimation(skeleton, interpolateWeightTo1, interruptingClipInfo[c], interruptingStateInfo, layer, layerWeight, add, interruptingClipTimeAddition, true, 1.0f)) { ++c; break; } } } // Mix the rest for (; c < interruptingClipInfoCount; c++) { appliedCount++; ApplyInterruptionAnimation(skeleton, interpolateWeightTo1, interruptingClipInfo[c], interruptingStateInfo, layer, layerWeight, add, interruptingClipTimeAddition); } } } } } /// /// Resolve matching weights from Unity's absolute weights A*w1 + B*w2 + C*w3 to /// Spine's lerp(lerp(A, B, x), C, y) weights, in reverse order of clips. /// protected void MatchWeights (ClipInfos clipInfos, bool hasNext, bool isInterruptionActive, int clipInfoCount, int nextClipInfoCount, int interruptingClipInfoCount, IList clipInfo, IList nextClipInfo, IList interruptingClipInfo) { if (clipInfos.clipResolvedWeights.Length < clipInfoCount) { System.Array.Resize(ref clipInfos.clipResolvedWeights, clipInfoCount); } if (hasNext && clipInfos.nextClipResolvedWeights.Length < nextClipInfoCount) { System.Array.Resize(ref clipInfos.nextClipResolvedWeights, nextClipInfoCount); } if (isInterruptionActive && clipInfos.interruptingClipResolvedWeights.Length < interruptingClipInfoCount) { System.Array.Resize(ref clipInfos.interruptingClipResolvedWeights, interruptingClipInfoCount); } float inverseWeight = 1.0f; if (isInterruptionActive) { for (int c = interruptingClipInfoCount - 1; c >= 0; c--) { float unityWeight = interruptingClipInfo[c].weight; clipInfos.interruptingClipResolvedWeights[c] = interruptingClipInfo[c].weight * inverseWeight; inverseWeight /= (1.0f - unityWeight); } } if (hasNext) { for (int c = nextClipInfoCount - 1; c >= 0; c--) { float unityWeight = nextClipInfo[c].weight; clipInfos.nextClipResolvedWeights[c] = nextClipInfo[c].weight * inverseWeight; inverseWeight /= (1.0f - unityWeight); } } for (int c = clipInfoCount - 1; c >= 0; c--) { float unityWeight = clipInfo[c].weight; clipInfos.clipResolvedWeights[c] = (c == 0) ? 1f : clipInfo[c].weight * inverseWeight; inverseWeight /= (1.0f - unityWeight); } } public KeyValuePair GetActiveAnimationAndTime (int layer) { if (layer >= layerClipInfos.Length) return new KeyValuePair(null, 0); ClipInfos layerInfos = layerClipInfos[layer]; bool isInterruptionActive = layerInfos.isInterruptionActive; ClipInfo clipInfo; Spine.Animation animation = null; AnimatorStateInfo stateInfo; if (isInterruptionActive && layerInfos.interruptingClipInfoCount > 0) { clipInfo = layerInfos.interruptingClipInfos[0]; stateInfo = layerInfos.interruptingStateInfo; } else if (layerInfos.clipInfoCount > 0) { clipInfo = layerInfos.clipInfos[0]; stateInfo = layerInfos.stateInfo; } else { return new KeyValuePair(null, 0); } animation = clipInfo.animation; float time = AnimationTime(stateInfo.normalizedTime, clipInfo.length, clipInfo.isLooping, stateInfo.speed < 0); return new KeyValuePair(animation, time); } static float AnimationTime (float normalizedTime, float clipLength, bool loop, bool reversed) { float time = AnimationTime(normalizedTime, clipLength, reversed); 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); if (normalizedTime < 0.0f) normalizedTime = (normalizedTime % 1.0f) + 1.0f; return normalizedTime * clipLength; } void InitClipInfosForLayers () { if (layerClipInfos.Length < layerCount) { System.Array.Resize(ref layerClipInfos, layerCount); for (int layer = 0, n = 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(); } } } private MixMode GetMixMode (int layer, bool layerIsAdditive) { if (useCustomMixMode) { MixMode mode = layerMixModes[layer]; // Note: at additive blending it makes no sense to use constant weight 1 at a fadeout anim add1 as // with override layers, so we use AlwaysMix instead to use the proper weights. // AlwaysMix leads to the expected result = lower_layer + lerp(add1, add2, transition_weight). if (layerIsAdditive && mode == MixMode.MixNext) { mode = MixMode.AlwaysMix; layerMixModes[layer] = mode; } return mode; } else { return layerIsAdditive ? MixMode.AlwaysMix : MixMode.MixNext; } } #if UNITY_EDITOR public void TransferDeprecatedFields () { MigrateLayerBlendModes(); } #endif void MigrateLayerBlendModes () { if (layerIsAdditive.Length == 0 && layerBlendModesDeprecated.Length > 0) { layerIsAdditive = new bool[layerBlendModesDeprecated.Length]; for (int i = 0; i < layerBlendModesDeprecated.Length; i++) layerIsAdditive[i] = layerBlendModesDeprecated[i] == 3; // MixBlend.Add was 3. layerBlendModesDeprecated = new int[0]; } } #if UNITY_EDITOR void GetLayerBlendModes () { if (layerIsAdditive.Length < layerCount) { System.Array.Resize(ref layerIsAdditive, layerCount); } AnimatorController controller = animator.runtimeAnimatorController as AnimatorController; for (int layer = 0, n = layerCount; layer < n; ++layer) { if (controller != null) layerIsAdditive[layer] = controller.layers[layer].blendingMode == AnimatorLayerBlendingMode.Additive; } } #endif void GetStateUpdatesFromAnimator (int layer) { ClipInfos layerInfos = layerClipInfos[layer]; // Note: Animator.GetLayerWeight always returns 0 on the first layer. Should be interpreted as 1. layerInfos.layerWeight = (layer == 0) ? 1 : animator.GetLayerWeight(layer); int clipInfoCount = animator.GetCurrentAnimatorClipInfoCount(layer); int nextClipInfoCount = animator.GetNextAnimatorClipInfoCount(layer); List clipInfos = layerInfos.clipInfos; List nextClipInfos = layerInfos.nextClipInfos; List interruptingClipInfos = layerInfos.interruptingClipInfos; layerInfos.isInterruptionActive = (clipInfoCount == 0 && clipInfos.Count != 0 && nextClipInfoCount == 0 && nextClipInfos.Count != 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). AnimatorStateInfo interruptingStateInfo = animator.GetNextAnimatorStateInfo(layer); layerInfos.isLastFrameOfInterruption = interruptingStateInfo.fullPathHash == 0; if (!layerInfos.isLastFrameOfInterruption) { List tempInterruptingClipInfos = new List(); animator.GetNextAnimatorClipInfo(layer, tempInterruptingClipInfos); interruptingClipInfos.Clear(); for (int i = 0; i < tempInterruptingClipInfos.Count; i++) { AnimatorClipInfo animatorInfo = tempInterruptingClipInfos[i]; ClipInfo info = new ClipInfo(); info.animation = GetAnimation(animatorInfo.clip); info.weight = animatorInfo.weight; info.length = animatorInfo.clip.length; info.isLooping = animatorInfo.clip.isLooping; interruptingClipInfos.Add(info); } layerInfos.interruptingClipInfoCount = interruptingClipInfos.Count; 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; // Get current clip infos and extract data List tempClipInfos = new List(); animator.GetCurrentAnimatorClipInfo(layer, tempClipInfos); clipInfos.Clear(); for (int i = 0; i < tempClipInfos.Count; i++) { AnimatorClipInfo animatorInfo = tempClipInfos[i]; ClipInfo info = new ClipInfo(); info.animation = GetAnimation(animatorInfo.clip); info.weight = animatorInfo.weight; info.length = animatorInfo.clip.length; info.isLooping = animatorInfo.clip.isLooping; clipInfos.Add(info); } // Get next clip infos and extract data List tempNextClipInfos = new List(); animator.GetNextAnimatorClipInfo(layer, tempNextClipInfos); nextClipInfos.Clear(); for (int i = 0; i < tempNextClipInfos.Count; i++) { AnimatorClipInfo animatorInfo = tempNextClipInfos[i]; ClipInfo info = new ClipInfo(); info.animation = GetAnimation(animatorInfo.clip); info.weight = animatorInfo.weight; info.length = animatorInfo.clip.length; info.isLooping = animatorInfo.clip.isLooping; nextClipInfos.Add(info); } 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) { ClipInfos 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) { ClipInfos 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(); #if USES_ENTITY_ID public bool Equals (AnimationClip x, AnimationClip y) { return x.GetEntityId() == y.GetEntityId(); } public int GetHashCode (AnimationClip o) { return o.GetHashCode(); } #else public bool Equals (AnimationClip x, AnimationClip y) { return x.GetInstanceID() == y.GetInstanceID(); } public int GetHashCode (AnimationClip o) { return o.GetInstanceID(); } #endif } 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; } } } } }