diff --git a/spine-unity/Modules/com.esotericsoftware.spine.timeline/Editor/SpineAnimationStateDrawer.cs b/spine-unity/Modules/com.esotericsoftware.spine.timeline/Editor/SpineAnimationStateDrawer.cs index 08e26e638..e3ab22206 100644 --- a/spine-unity/Modules/com.esotericsoftware.spine.timeline/Editor/SpineAnimationStateDrawer.cs +++ b/spine-unity/Modules/com.esotericsoftware.spine.timeline/Editor/SpineAnimationStateDrawer.cs @@ -51,7 +51,8 @@ public class SpineAnimationStateDrawer : PropertyDrawer { SerializedProperty mixDurationProp = property.FindPropertyRelative("mixDuration"); SerializedProperty holdPreviousProp = property.FindPropertyRelative("holdPrevious"); SerializedProperty dontPauseWithDirectorProp = property.FindPropertyRelative("dontPauseWithDirector"); - SerializedProperty dontPauseOnStopProp = property.FindPropertyRelative("dontPauseOnStop"); + SerializedProperty dontEndWithClip = property.FindPropertyRelative("dontEndWithClip"); + SerializedProperty endMixOutDuration = property.FindPropertyRelative("endMixOutDuration"); SerializedProperty eventProp = property.FindPropertyRelative("eventThreshold"); SerializedProperty attachmentProp = property.FindPropertyRelative("attachmentThreshold"); SerializedProperty drawOrderProp = property.FindPropertyRelative("drawOrderThreshold"); @@ -78,11 +79,19 @@ public class SpineAnimationStateDrawer : PropertyDrawer { "If set to true, the animation will continue playing when the Director is paused.")); singleFieldRect.y += lineHeightWithSpacing; + EditorGUI.PropertyField(singleFieldRect, dontEndWithClip, + new GUIContent("Don't End with Clip", + "Normally when empty space follows the clip on the timeline, the empty animation is set on the track. " + + "Set this parameter to true to continue playing the clip's animation instead.")); - using (new EditorGUI.DisabledGroupScope(dontPauseWithDirectorProp.boolValue == true)) { - EditorGUI.PropertyField(singleFieldRect, dontPauseOnStopProp, - new GUIContent("Don't Pause on Stop", - "If 'Don't Pause with Director' is true but this parameter is false, the animation will continue playing when the Graph is stopped, e.g. when reaching the track end.")); + singleFieldRect.y += lineHeightWithSpacing; + + using (new EditorGUI.DisabledGroupScope(dontEndWithClip.boolValue == true)) { + EditorGUI.PropertyField(singleFieldRect, endMixOutDuration, + new GUIContent("Clip End Mix Out Duration", + "When 'Don't End with Clip' is false, and the clip is followed by blank space or stopped, " + + "the empty animation is set with this MixDuration. When set to a negative value, " + + "the clip is paused instead.")); } singleFieldRect.y += lineHeightWithSpacing * 0.5f; diff --git a/spine-unity/Modules/com.esotericsoftware.spine.timeline/Runtime/SpineAnimationState/SpineAnimationStateBehaviour.cs b/spine-unity/Modules/com.esotericsoftware.spine.timeline/Runtime/SpineAnimationState/SpineAnimationStateBehaviour.cs index 05ec91c8b..f6f7047fd 100644 --- a/spine-unity/Modules/com.esotericsoftware.spine.timeline/Runtime/SpineAnimationState/SpineAnimationStateBehaviour.cs +++ b/spine-unity/Modules/com.esotericsoftware.spine.timeline/Runtime/SpineAnimationState/SpineAnimationStateBehaviour.cs @@ -54,7 +54,9 @@ namespace Spine.Unity.Playables { public float mixDuration = 0.1f; public bool holdPrevious = false; public bool dontPauseWithDirector = false; - public bool dontPauseOnStop = false; + [UnityEngine.Serialization.FormerlySerializedAs("dontPauseOnStop")] + public bool dontEndWithClip = false; + public float endMixOutDuration = 0.1f; [Range(0, 1f)] public float attachmentThreshold = 0.5f; diff --git a/spine-unity/Modules/com.esotericsoftware.spine.timeline/Runtime/SpineAnimationState/SpineAnimationStateMixerBehaviour.cs b/spine-unity/Modules/com.esotericsoftware.spine.timeline/Runtime/SpineAnimationState/SpineAnimationStateMixerBehaviour.cs index 4e6066655..33759637c 100644 --- a/spine-unity/Modules/com.esotericsoftware.spine.timeline/Runtime/SpineAnimationState/SpineAnimationStateMixerBehaviour.cs +++ b/spine-unity/Modules/com.esotericsoftware.spine.timeline/Runtime/SpineAnimationState/SpineAnimationStateMixerBehaviour.cs @@ -38,15 +38,19 @@ namespace Spine.Unity.Playables { public class SpineAnimationStateMixerBehaviour : PlayableBehaviour { float[] lastInputWeights; + bool lastAnyTrackPlaying = false; public int trackIndex; IAnimationStateComponent animationStateComponent; bool pauseWithDirector = true; - bool pauseWithDirectorButNotOnStop = false; + bool endAtClipEnd = true; + float endMixOutDuration = 0.1f; bool isPaused = false; TrackEntry pausedTrackEntry; float previousTimeScale = 1; + TrackEntry timelineStartedTrackEntry; + public override void OnBehaviourPause (Playable playable, FrameData info) { if (pauseWithDirector) { if (!isPaused) @@ -56,8 +60,10 @@ namespace Spine.Unity.Playables { } public override void OnGraphStop (Playable playable) { - if (isPaused && pauseWithDirectorButNotOnStop) - HandleResume(playable); // this stop event occurs after pause, so resume again + if (endAtClipEnd) + HandleClipEnd(); + else if (isPaused) // stop event occurred after pause, so resume again + HandleResume(playable); } public override void OnBehaviourPlay (Playable playable, FrameData info) { @@ -70,7 +76,7 @@ namespace Spine.Unity.Playables { if (animationStateComponent == null) return; TrackEntry current = animationStateComponent.AnimationState.GetCurrent(trackIndex); - if (current != null) { + if (current != null && current == timelineStartedTrackEntry) { previousTimeScale = current.TimeScale; current.TimeScale = 0; pausedTrackEntry = current; @@ -86,9 +92,22 @@ namespace Spine.Unity.Playables { } } + protected void HandleClipEnd () { + var state = animationStateComponent.AnimationState; + if (endAtClipEnd && + timelineStartedTrackEntry != null && + timelineStartedTrackEntry == state.GetCurrent(trackIndex)) { + + if (endMixOutDuration >= 0) + state.SetEmptyAnimation(trackIndex, endMixOutDuration); + else // pause if endMixOutDuration < 0 + timelineStartedTrackEntry.TimeScale = 0; + timelineStartedTrackEntry = null; + } + } + // NOTE: This function is called at runtime and edit time. Keep that in mind when setting the values of properties. public override void ProcessFrame (Playable playable, FrameData info, object playerData) { - var skeletonAnimation = playerData as SkeletonAnimation; var skeletonGraphic = playerData as SkeletonGraphic; animationStateComponent = playerData as IAnimationStateComponent; @@ -116,12 +135,15 @@ namespace Spine.Unity.Playables { this.lastInputWeights[i] = default(float); } var lastInputWeights = this.lastInputWeights; + bool anyTrackPlaying = false; // Check all clips. If a clip that was weight 0 turned into weight 1, call SetAnimation. for (int i = 0; i < inputCount; i++) { float lastInputWeight = lastInputWeights[i]; float inputWeight = playable.GetInputWeight(i); bool trackStarted = lastInputWeight == 0 && inputWeight > 0; + if (inputWeight > 0) + anyTrackPlaying = true; lastInputWeights[i] = inputWeight; if (trackStarted) { @@ -129,7 +151,8 @@ namespace Spine.Unity.Playables { SpineAnimationStateBehaviour clipData = inputPlayable.GetBehaviour(); pauseWithDirector = !clipData.dontPauseWithDirector; - pauseWithDirectorButNotOnStop = pauseWithDirector && clipData.dontPauseOnStop; + endAtClipEnd = !clipData.dontEndWithClip; + endMixOutDuration = clipData.endMixOutDuration; if (clipData.animationReference == null) { float mixDuration = clipData.customDuration ? clipData.mixDuration : state.Data.DefaultMix; @@ -147,6 +170,8 @@ namespace Spine.Unity.Playables { if (clipData.customDuration) trackEntry.MixDuration = clipData.mixDuration; + + timelineStartedTrackEntry = trackEntry; } //else Debug.LogWarningFormat("Animation named '{0}' not found", clipData.animationName); } @@ -162,6 +187,9 @@ namespace Spine.Unity.Playables { } } } + if (lastAnyTrackPlaying && !anyTrackPlaying) + HandleClipEnd(); + this.lastAnyTrackPlaying = anyTrackPlaying; } #if SPINE_EDITMODEPOSE