/****************************************************************************** * Spine Runtimes License Agreement * Last updated April 5, 2025. Replaces all prior versions. * * Copyright (c) 2013-2025, 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. *****************************************************************************/ using Spine; using Spine.Unity; using System.Collections; using System.Collections.Generic; using UnityEngine; namespace Spine.Unity.Examples { //[CreateAssetMenu(menuName = "Spine/SkeletonData Modifiers/Animation Match", order = 200)] public class AnimationMatchModifierAsset : SkeletonDataModifierAsset { public bool matchAllAnimations = true; public override void Apply (SkeletonData skeletonData) { if (matchAllAnimations) AnimationTools.MatchAnimationTimelines(skeletonData.Animations, skeletonData); } public static class AnimationTools { #region Filler Timelines /// /// Matches the animation timelines across the given set of animations. /// This allows unkeyed properties to assume setup pose when animations are naively mixed using Animation.Apply. /// /// An enumerable collection animations whose timelines will be matched. /// The SkeletonData where the animations belong. public static void MatchAnimationTimelines (IEnumerable animations, SkeletonData skeletonData) { if (animations == null) return; if (skeletonData == null) throw new System.ArgumentNullException("skeletonData", "Timelines can't be matched without a SkeletonData source."); // Build a reference collection of timelines to match // and a collection of dummy timelines that can be used to fill-in missing items. Dictionary timelineDictionary = new Dictionary(); foreach (Animation animation in animations) { foreach (Timeline timeline in animation.Timelines) { if (timeline is EventTimeline) continue; foreach (string propertyId in timeline.PropertyIds) { if (!timelineDictionary.ContainsKey(propertyId)) { timelineDictionary.Add(propertyId, GetFillerTimeline(timeline, skeletonData)); } } } } List idsToMatch = new List(timelineDictionary.Keys); // For each animation in the list, check for and add missing timelines. HashSet currentAnimationIDs = new HashSet(); foreach (Animation animation in animations) { currentAnimationIDs.Clear(); foreach (Timeline timeline in animation.Timelines) { if (timeline is EventTimeline) continue; foreach (string propertyId in timeline.PropertyIds) { currentAnimationIDs.Add(propertyId); } } ExposedList animationTimelines = animation.Timelines; foreach (string propertyId in idsToMatch) { if (!currentAnimationIDs.Contains(propertyId)) animationTimelines.Add(timelineDictionary[propertyId]); } } // These are locals, but sometimes Unity's GC does weird stuff. So let's clean up. timelineDictionary.Clear(); timelineDictionary = null; idsToMatch.Clear(); idsToMatch = null; currentAnimationIDs.Clear(); currentAnimationIDs = null; } static Timeline GetFillerTimeline (Timeline timeline, SkeletonData skeletonData) { if (timeline is RotateTimeline) return GetFillerTimeline((RotateTimeline)timeline, skeletonData); if (timeline is TranslateTimeline) return GetFillerTimeline((TranslateTimeline)timeline, skeletonData); if (timeline is ScaleTimeline) return GetFillerTimeline((ScaleTimeline)timeline, skeletonData); if (timeline is ShearTimeline) return GetFillerTimeline((ShearTimeline)timeline, skeletonData); if (timeline is AttachmentTimeline) return GetFillerTimeline((AttachmentTimeline)timeline, skeletonData); if (timeline is RGBATimeline) return GetFillerTimeline((RGBATimeline)timeline, skeletonData); if (timeline is RGBA2Timeline) return GetFillerTimeline((RGBA2Timeline)timeline, skeletonData); if (timeline is DeformTimeline) return GetFillerTimeline((DeformTimeline)timeline, skeletonData); if (timeline is DrawOrderTimeline) return GetFillerTimeline((DrawOrderTimeline)timeline, skeletonData); if (timeline is IkConstraintTimeline) return GetFillerTimeline((IkConstraintTimeline)timeline, skeletonData); if (timeline is TransformConstraintTimeline) return GetFillerTimeline((TransformConstraintTimeline)timeline, skeletonData); if (timeline is PathConstraintPositionTimeline) return GetFillerTimeline((PathConstraintPositionTimeline)timeline, skeletonData); if (timeline is PathConstraintSpacingTimeline) return GetFillerTimeline((PathConstraintSpacingTimeline)timeline, skeletonData); if (timeline is PathConstraintMixTimeline) return GetFillerTimeline((PathConstraintMixTimeline)timeline, skeletonData); return null; } static RotateTimeline GetFillerTimeline (RotateTimeline timeline, SkeletonData skeletonData) { RotateTimeline t = new RotateTimeline(1, 0, timeline.BoneIndex); t.SetFrame(0, 0, 0); return t; } static TranslateTimeline GetFillerTimeline (TranslateTimeline timeline, SkeletonData skeletonData) { TranslateTimeline t = new TranslateTimeline(1, 0, timeline.BoneIndex); t.SetFrame(0, 0, 0, 0); return t; } static ScaleTimeline GetFillerTimeline (ScaleTimeline timeline, SkeletonData skeletonData) { ScaleTimeline t = new ScaleTimeline(1, 0, timeline.BoneIndex); t.SetFrame(0, 0, 0, 0); return t; } static ShearTimeline GetFillerTimeline (ShearTimeline timeline, SkeletonData skeletonData) { ShearTimeline t = new ShearTimeline(1, 0, timeline.BoneIndex); t.SetFrame(0, 0, 0, 0); return t; } static AttachmentTimeline GetFillerTimeline (AttachmentTimeline timeline, SkeletonData skeletonData) { AttachmentTimeline t = new AttachmentTimeline(1, timeline.SlotIndex); SlotData slotData = skeletonData.Slots.Items[t.SlotIndex]; t.SetFrame(0, 0, slotData.AttachmentName); return t; } static RGBATimeline GetFillerTimeline (RGBATimeline timeline, SkeletonData skeletonData) { RGBATimeline t = new RGBATimeline(1, 0, timeline.SlotIndex); SlotData slotData = skeletonData.Slots.Items[t.SlotIndex]; Color color = slotData.GetSetupPose().GetColor(); t.SetFrame(0, 0, color.r, color.g, color.b, color.a); return t; } static RGBA2Timeline GetFillerTimeline (RGBA2Timeline timeline, SkeletonData skeletonData) { RGBA2Timeline t = new RGBA2Timeline(1, 0, timeline.SlotIndex); SlotData slotData = skeletonData.Slots.Items[t.SlotIndex]; var setup = slotData.GetSetupPose(); Color color = setup.GetColor(); Color? darkColorOptional = setup.GetDarkColor(); Color darkColor = darkColorOptional.HasValue ? darkColorOptional.Value : Color.black; t.SetFrame(0, 0, color.r, color.g, color.b, color.a, darkColor.r, darkColor.g, darkColor.b); return t; } static DeformTimeline GetFillerTimeline (DeformTimeline timeline, SkeletonData skeletonData) { DeformTimeline t = new DeformTimeline(1, 0, timeline.SlotIndex, timeline.Attachment); if (t.Attachment.IsWeighted()) { t.SetFrame(0, 0, new float[t.Attachment.Vertices.Length]); } else { t.SetFrame(0, 0, t.Attachment.Vertices.Clone() as float[]); } return t; } static DrawOrderTimeline GetFillerTimeline (DrawOrderTimeline timeline, SkeletonData skeletonData) { DrawOrderTimeline t = new DrawOrderTimeline(1); t.SetFrame(0, 0, null); // null means use setup pose in DrawOrderTimeline.Apply. return t; } static IkConstraintTimeline GetFillerTimeline (IkConstraintTimeline timeline, SkeletonData skeletonData) { IkConstraintTimeline t = new IkConstraintTimeline(1, 0, timeline.ConstraintIndex); var ikConstraintData = (IkConstraintData)skeletonData.Constraints.Items[timeline.ConstraintIndex]; var setup = ikConstraintData.GetSetupPose(); t.SetFrame(0, 0, setup.Mix, setup.Softness, setup.BendDirection, setup.Compress, setup.Stretch); return t; } static TransformConstraintTimeline GetFillerTimeline (TransformConstraintTimeline timeline, SkeletonData skeletonData) { TransformConstraintTimeline t = new TransformConstraintTimeline(1, 0, timeline.ConstraintIndex); var data = (TransformConstraintData)skeletonData.Constraints.Items[timeline.ConstraintIndex]; var setup = data.GetSetupPose(); t.SetFrame(0, 0, setup.MixRotate, setup.MixX, setup.MixY, setup.MixScaleX, setup.MixScaleY, setup.MixShearY); return t; } static PathConstraintPositionTimeline GetFillerTimeline (PathConstraintPositionTimeline timeline, SkeletonData skeletonData) { PathConstraintPositionTimeline t = new PathConstraintPositionTimeline(1, 0, timeline.ConstraintIndex); var data = (PathConstraintData)skeletonData.Constraints.Items[timeline.ConstraintIndex]; var setup = data.GetSetupPose(); t.SetFrame(0, 0, setup.Position); return t; } static PathConstraintSpacingTimeline GetFillerTimeline (PathConstraintSpacingTimeline timeline, SkeletonData skeletonData) { PathConstraintSpacingTimeline t = new PathConstraintSpacingTimeline(1, 0, timeline.ConstraintIndex); var data = (PathConstraintData)skeletonData.Constraints.Items[timeline.ConstraintIndex]; var setup = data.GetSetupPose(); t.SetFrame(0, 0, setup.Spacing); return t; } static PathConstraintMixTimeline GetFillerTimeline (PathConstraintMixTimeline timeline, SkeletonData skeletonData) { PathConstraintMixTimeline t = new PathConstraintMixTimeline(1, 0, timeline.ConstraintIndex); PathConstraintData data = (PathConstraintData)skeletonData.Constraints.Items[timeline.ConstraintIndex]; var setup = data.GetSetupPose(); t.SetFrame(0, 0, setup.MixRotate, setup.MixX, setup.MixY); return t; } #endregion } } }