Browse Source

Merge branch '3.8' of https://github.com/EsotericSoftware/spine-runtimes into 3.8

badlogic 6 years ago
parent
commit
f619a972cb
17 changed files with 301 additions and 42 deletions
  1. 9 0
      spine-csharp/src/Animation.cs
  2. 2 9
      spine-csharp/src/AnimationState.cs
  3. 12 0
      spine-unity/Assets/Spine/Editor/spine-unity/Editor/Utility/Preferences.cs
  4. 15 5
      spine-unity/Assets/Spine/Editor/spine-unity/Editor/Windows/SkeletonBaker.cs
  5. 0 6
      spine-unity/Assets/Spine/Editor/spine-unity/Editor/Windows/SkeletonBakingWindow.cs
  6. 10 0
      spine-unity/Assets/Spine/Editor/spine-unity/Editor/Windows/SpinePreferences.cs
  7. 5 2
      spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonAnimation.cs
  8. 2 0
      spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonGraphic.cs
  9. 2 0
      spine-unity/Assets/Spine/Runtime/spine-unity/Utility/AtlasUtilities.cs
  10. 116 0
      spine-unity/Modules/com.esotericsoftware.spine.timeline/Editor/SpineAnimationStateClipInspector.cs
  11. 12 0
      spine-unity/Modules/com.esotericsoftware.spine.timeline/Editor/SpineAnimationStateClipInspector.cs.meta
  12. 29 8
      spine-unity/Modules/com.esotericsoftware.spine.timeline/Editor/SpineAnimationStateDrawer.cs
  13. 51 0
      spine-unity/Modules/com.esotericsoftware.spine.timeline/Editor/SpineSkeletonFlipClipInspector.cs
  14. 12 0
      spine-unity/Modules/com.esotericsoftware.spine.timeline/Editor/SpineSkeletonFlipClipInspector.cs.meta
  15. 4 1
      spine-unity/Modules/com.esotericsoftware.spine.timeline/Runtime/SpineAnimationState/SpineAnimationStateBehaviour.cs
  16. 9 1
      spine-unity/Modules/com.esotericsoftware.spine.timeline/Runtime/SpineAnimationState/SpineAnimationStateClip.cs
  17. 11 10
      spine-unity/Modules/com.esotericsoftware.spine.timeline/Runtime/SpineAnimationState/SpineAnimationStateMixerBehaviour.cs

+ 9 - 0
spine-csharp/src/Animation.cs

@@ -37,11 +37,15 @@ namespace Spine {
 	public class Animation {
 		internal String name;
 		internal ExposedList<Timeline> timelines;
+		internal HashSet<int> timelineIds;
 		internal float duration;
 
 		public Animation (string name, ExposedList<Timeline> timelines, float duration) {
 			if (name == null) throw new ArgumentNullException("name", "name cannot be null.");
 			if (timelines == null) throw new ArgumentNullException("timelines", "timelines cannot be null.");
+			this.timelineIds = new HashSet<int>();
+			foreach (Timeline timeline in timelines)
+				timelineIds.Add(timeline.PropertyId);
 			this.name = name;
 			this.timelines = timelines;
 			this.duration = duration;
@@ -55,6 +59,11 @@ namespace Spine {
 		/// <summary>The animation's name, which is unique across all animations in the skeleton.</summary>
 		public string Name { get { return name; } }
 
+		/// <summary>Whether the timeline with the property id is contained in this animation.</summary>
+		public bool HasTimeline (int id) {
+			return timelineIds.Contains(id);
+		}
+
 		/// <summary>Applies all the animation's timelines to the specified skeleton.</summary>
 		/// <seealso cref="Timeline.Apply(Skeleton, float, float, ExposedList, float, MixBlend, MixDirection)"/>
 		public void Apply (Skeleton skeleton, float lastTime, float time, bool loop, ExposedList<Event> events, float alpha, MixBlend blend,

+ 2 - 9
spine-csharp/src/AnimationState.cs

@@ -777,11 +777,11 @@ namespace Spine {
 				if (!propertyIDs.Add(id))
 					timelineMode[i] = AnimationState.Subsequent;
 				else if (to == null || timeline is AttachmentTimeline || timeline is DrawOrderTimeline
-						|| timeline is EventTimeline || !HasTimeline(to, id)) {
+						|| timeline is EventTimeline || !to.animation.HasTimeline(id)) {
 					timelineMode[i] = AnimationState.First;
 				} else {
 					for (TrackEntry next = to.mixingTo; next != null; next = next.mixingTo) {
-						if (HasTimeline(next, id)) continue;
+						if (next.animation.HasTimeline(id)) continue;
 						if (next.mixDuration > 0) {
 							timelineMode[i] = AnimationState.HoldMix;
 							timelineHoldMix[i] = next;
@@ -809,13 +809,6 @@ namespace Spine {
 			}
 		}
 
-		static bool HasTimeline (TrackEntry entry, int id) {
-			var timelines = entry.animation.timelines.Items;
-			for (int i = 0, n = entry.animation.timelines.Count; i < n; i++)
-				if (timelines[i].PropertyId == id) return true;
-			return false;
-		}
-
 		/// <returns>The track entry for the animation currently playing on the track, or null if no animation is currently playing.</returns>
 		public TrackEntry GetCurrent (int trackIndex) {
 			if (trackIndex >= tracks.Count) return null;

+ 12 - 0
spine-unity/Assets/Spine/Editor/spine-unity/Editor/Utility/Preferences.cs

@@ -138,6 +138,9 @@ namespace Spine.Unity.Editor {
 			const string MECANIM_EVENT_INCLUDE_FOLDERNAME_KEY = "SPINE_MECANIM_EVENT_INCLUDE_FOLDERNAME";
 			public static bool mecanimEventIncludeFolderName = SpinePreferences.DEFAULT_MECANIM_EVENT_INCLUDE_FOLDERNAME;
 
+			const string TIMELINE_USE_BLEND_DURATION_KEY = "SPINE_TIMELINE_USE_BLEND_DURATION_KEY";
+			public static bool timelineUseBlendDuration = SpinePreferences.DEFAULT_TIMELINE_USE_BLEND_DURATION;
+
 			static bool preferencesLoaded = false;
 
 			public static void Load () {
@@ -154,6 +157,7 @@ namespace Spine.Unity.Editor {
 				mecanimEventIncludeFolderName = EditorPrefs.GetBool(MECANIM_EVENT_INCLUDE_FOLDERNAME_KEY, SpinePreferences.DEFAULT_MECANIM_EVENT_INCLUDE_FOLDERNAME);
 				atlasTxtImportWarning = EditorPrefs.GetBool(ATLASTXT_WARNING_KEY, SpinePreferences.DEFAULT_ATLASTXT_WARNING);
 				textureImporterWarning = EditorPrefs.GetBool(TEXTUREIMPORTER_WARNING_KEY, SpinePreferences.DEFAULT_TEXTUREIMPORTER_WARNING);
+				timelineUseBlendDuration = EditorPrefs.GetBool(TIMELINE_USE_BLEND_DURATION_KEY, SpinePreferences.DEFAULT_TIMELINE_USE_BLEND_DURATION);
 
 				SpineHandles.handleScale = EditorPrefs.GetFloat(SCENE_ICONS_SCALE_KEY, DEFAULT_SCENE_ICONS_SCALE);
 				preferencesLoaded = true;
@@ -171,6 +175,7 @@ namespace Spine.Unity.Editor {
 				newPreferences.mecanimEventIncludeFolderName = EditorPrefs.GetBool(MECANIM_EVENT_INCLUDE_FOLDERNAME_KEY, SpinePreferences.DEFAULT_MECANIM_EVENT_INCLUDE_FOLDERNAME);
 				newPreferences.atlasTxtImportWarning = EditorPrefs.GetBool(ATLASTXT_WARNING_KEY, SpinePreferences.DEFAULT_ATLASTXT_WARNING);
 				newPreferences.textureImporterWarning = EditorPrefs.GetBool(TEXTUREIMPORTER_WARNING_KEY, SpinePreferences.DEFAULT_TEXTUREIMPORTER_WARNING);
+				newPreferences.timelineUseBlendDuration = EditorPrefs.GetBool(TIMELINE_USE_BLEND_DURATION_KEY, SpinePreferences.DEFAULT_TIMELINE_USE_BLEND_DURATION);
 			}
 
 			public static void SaveToEditorPrefs(SpinePreferences preferences) {
@@ -184,6 +189,7 @@ namespace Spine.Unity.Editor {
 				EditorPrefs.SetBool(MECANIM_EVENT_INCLUDE_FOLDERNAME_KEY, preferences.mecanimEventIncludeFolderName);
 				EditorPrefs.SetBool(ATLASTXT_WARNING_KEY, preferences.atlasTxtImportWarning);
 				EditorPrefs.SetBool(TEXTUREIMPORTER_WARNING_KEY, preferences.textureImporterWarning);
+				EditorPrefs.SetBool(TIMELINE_USE_BLEND_DURATION_KEY, preferences.timelineUseBlendDuration);
 			}
 #endif
 
@@ -265,6 +271,12 @@ namespace Spine.Unity.Editor {
 					if (GUILayout.Button("Disable", GUILayout.Width(64)))
 						SpineTK2DEditorUtility.DisableTK2D();
 				}
+
+				GUILayout.Space(20);
+				EditorGUILayout.LabelField("Timeline Extension", EditorStyles.boldLabel);
+				{
+					SpineEditorUtilities.BoolPrefsField(ref timelineUseBlendDuration, TIMELINE_USE_BLEND_DURATION_KEY, new GUIContent("Use Blend Duration", "When enabled, MixDuration will be synced with timeline clip transition duration 'Ease In Duration'."));
+				}
 			}
 		#endif // !NEW_PREFERENCES_SETTINGS_PROVIDER
 		}

+ 15 - 5
spine-unity/Assets/Spine/Editor/spine-unity/Editor/Windows/SkeletonBaker.cs

@@ -184,9 +184,7 @@ namespace Spine.Unity.Editor {
 				Debug.LogError("Could not export Spine Skeleton because SkeletonDataAsset is null or invalid!");
 				return;
 			}
-
-			#if !NEW_PREFAB_SYSTEM
-
+			
 			if (outputPath == "") {
 				outputPath = System.IO.Path.GetDirectoryName(AssetDatabase.GetAssetPath(skeletonDataAsset)) + "/Baked";
 				System.IO.Directory.CreateDirectory(outputPath);
@@ -281,7 +279,13 @@ namespace Spine.Unity.Editor {
 				Object prefab = AssetDatabase.LoadAssetAtPath(prefabPath, typeof(GameObject));
 
 				if (prefab == null) {
+					#if NEW_PREFAB_SYSTEM
+					GameObject emptyGameObject = new GameObject();
+					prefab = PrefabUtility.SaveAsPrefabAssetAndConnect(emptyGameObject, prefabPath, InteractionMode.AutomatedAction);
+					GameObject.DestroyImmediate(emptyGameObject);
+					#else
 					prefab = PrefabUtility.CreateEmptyPrefab(prefabPath);
+					#endif
 					newPrefab = true;
 				}
 
@@ -428,14 +432,22 @@ namespace Spine.Unity.Editor {
 				}
 
 				if (newPrefab) {
+					#if NEW_PREFAB_SYSTEM
+					PrefabUtility.SaveAsPrefabAssetAndConnect(prefabRoot, prefabPath, InteractionMode.AutomatedAction);
+					#else
 					PrefabUtility.ReplacePrefab(prefabRoot, prefab, ReplacePrefabOptions.ConnectToPrefab);
+					#endif
 				} else {
 
 					foreach (string str in unusedMeshNames) {
 						Mesh.DestroyImmediate(meshTable[str], true);
 					}
 
+					#if NEW_PREFAB_SYSTEM
+					PrefabUtility.SaveAsPrefabAssetAndConnect(prefabRoot, prefabPath, InteractionMode.AutomatedAction);
+					#else
 					PrefabUtility.ReplacePrefab(prefabRoot, prefab, ReplacePrefabOptions.ReplaceNameBased);
+					#endif
 				}
 
 
@@ -447,8 +459,6 @@ namespace Spine.Unity.Editor {
 				GameObject.DestroyImmediate(prefabRoot);
 
 			}
-			#endif
-
 		}
 
 		#region Attachment Baking

+ 0 - 6
spine-unity/Assets/Spine/Editor/spine-unity/Editor/Windows/SkeletonBakingWindow.cs

@@ -27,10 +27,6 @@
  * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  *****************************************************************************/
 
-#if UNITY_2018_3 || UNITY_2019 || UNITY_2018_3_OR_NEWER
-#define NEW_PREFAB_SYSTEM
-#endif
-
 using System.Collections;
 using System.Collections.Generic;
 using UnityEngine;
@@ -44,7 +40,6 @@ namespace Spine.Unity.Editor {
 	public class SkeletonBakingWindow : EditorWindow {
 		const bool IsUtilityWindow = true;
 
-		#if !NEW_PREFAB_SYSTEM
 		[MenuItem("CONTEXT/SkeletonDataAsset/Skeleton Baking", false, 5000)]
 		public static void Init (MenuCommand command) {
 			var window = EditorWindow.GetWindow<SkeletonBakingWindow>(IsUtilityWindow);
@@ -54,7 +49,6 @@ namespace Spine.Unity.Editor {
 			window.skeletonDataAsset = command.context as SkeletonDataAsset;
 			window.Show();
 		}
-		#endif
 
 		public SkeletonDataAsset skeletonDataAsset;
 		[SpineSkin(dataField:"skeletonDataAsset")]

+ 10 - 0
spine-unity/Assets/Spine/Editor/spine-unity/Editor/Windows/SpinePreferences.cs

@@ -92,6 +92,10 @@ namespace Spine.Unity.Editor {
 		public const bool DEFAULT_MECANIM_EVENT_INCLUDE_FOLDERNAME = true;
 		public bool mecanimEventIncludeFolderName = DEFAULT_MECANIM_EVENT_INCLUDE_FOLDERNAME;
 
+		// Timeline extension module
+		public const bool DEFAULT_TIMELINE_USE_BLEND_DURATION = true;
+		public bool timelineUseBlendDuration = DEFAULT_TIMELINE_USE_BLEND_DURATION;
+
 #if NEW_PREFERENCES_SETTINGS_PROVIDER
 		public static void Load () {
 			SpineHandles.handleScale = EditorPrefs.GetFloat(SCENE_ICONS_SCALE_KEY, DEFAULT_SCENE_ICONS_SCALE);
@@ -185,6 +189,12 @@ namespace Spine.Unity.Editor {
 					if (GUILayout.Button("Disable", GUILayout.Width(64)))
 						SpineEditorUtilities.SpineTK2DEditorUtility.DisableTK2D();
 				}
+
+				GUILayout.Space(20);
+				EditorGUILayout.LabelField("Timeline Extension", EditorStyles.boldLabel);
+				{
+					EditorGUILayout.PropertyField(settings.FindProperty("timelineUseBlendDuration"), new GUIContent("Use Blend Duration", "When enabled, MixDuration will be synced with timeline clip transition duration 'Ease In Duration'."));
+				}
 			}
 			EditorGUIUtility.labelWidth = prevLabelWidth;
 		}

+ 5 - 2
spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonAnimation.cs

@@ -96,8 +96,11 @@ namespace Spine.Unity {
 				}
 			}
 			set {
-				if (_animationName == value)
-					return;
+				if (_animationName == value) {
+					TrackEntry entry = state.GetCurrent(0);
+					if (entry != null && entry.loop == loop)
+						return;
+				}
 				_animationName = value;
 
 				if (string.IsNullOrEmpty(value)) {

+ 2 - 0
spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonGraphic.cs

@@ -288,6 +288,8 @@ namespace Spine.Unity {
 					if (!Application.isPlaying)
 						Update(0f);
 					#endif
+					if (freeze)
+						Update(0f);
 				}
 			}
 		}

+ 2 - 0
spine-unity/Assets/Spine/Runtime/spine-unity/Utility/AtlasUtilities.cs

@@ -580,7 +580,9 @@ namespace Spine.Unity.AttachmentTools {
 		static void CopyTextureAttributesFrom(this Texture2D destination, Texture2D source) {
 			destination.filterMode = source.filterMode;
 			destination.anisoLevel = source.anisoLevel;
+		#if UNITY_EDITOR
 			destination.alphaIsTransparency = source.alphaIsTransparency;
+		#endif
 			destination.wrapModeU = source.wrapModeU;
 			destination.wrapModeV = source.wrapModeV;
 			destination.wrapModeW = source.wrapModeW;

+ 116 - 0
spine-unity/Modules/com.esotericsoftware.spine.timeline/Editor/SpineAnimationStateClipInspector.cs

@@ -0,0 +1,116 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated May 1, 2019. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2019, 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.
+ *
+ * THIS SOFTWARE IS 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 THIS SOFTWARE,
+ * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+using UnityEditor;
+using Spine.Unity.Playables;
+using UnityEngine.Timeline;
+
+namespace Spine.Unity.Editor {
+
+	[CustomEditor(typeof(SpineAnimationStateClip))]
+	[CanEditMultipleObjects]
+	public class SpineAnimationStateClipInspector : UnityEditor.Editor {
+
+		protected SerializedProperty templateProp = null;
+
+		protected class ClipInfo {
+			public TimelineClip timelineClip;
+			public float previousBlendInDuration = -1.0f;
+			public float unblendedMixDuration = 0.2f;
+		}
+
+		protected ClipInfo[] clipInfo = null;
+		
+		public void OnEnable () {
+			templateProp = serializedObject.FindProperty("template");
+			System.Array.Resize(ref clipInfo, targets.Length);
+			for (int i = 0; i < targets.Length; ++i) {
+				var clip = (SpineAnimationStateClip)targets[i];
+				clipInfo[i] = new ClipInfo();
+				clipInfo[i].timelineClip = FindTimelineClip(clip);
+			}
+		}
+
+		public override void OnInspectorGUI () {
+			serializedObject.Update();
+			EditorGUILayout.PropertyField(templateProp);
+
+			for (int i = 0; i < targets.Length; ++i) {
+				var targetClip = (SpineAnimationStateClip)targets[i];
+				if (targetClip.template.useBlendDuration)
+					AdjustMixDuration(targetClip, clipInfo[i]);
+			}
+			
+			serializedObject.ApplyModifiedProperties();
+		}
+
+		protected void AdjustMixDuration(SpineAnimationStateClip targetClip, ClipInfo timelineClipInfo) {
+
+			if (timelineClipInfo == null)
+				return;
+
+			var timelineClip = timelineClipInfo.timelineClip;
+			if (timelineClip == null)
+				return;
+
+			float blendInDur = (float)timelineClip.blendInDuration;
+			bool isBlendingNow = blendInDur > 0;
+			bool wasBlendingBefore = timelineClipInfo.previousBlendInDuration > 0;
+
+			if (isBlendingNow) {
+				if (!wasBlendingBefore) {
+					timelineClipInfo.unblendedMixDuration = targetClip.template.mixDuration;
+				}
+				targetClip.template.mixDuration = blendInDur;
+				EditorUtility.SetDirty(targetClip);
+			}
+			else if (wasBlendingBefore) {
+				targetClip.template.mixDuration = timelineClipInfo.unblendedMixDuration;
+				EditorUtility.SetDirty(targetClip);
+			}
+			timelineClipInfo.previousBlendInDuration = blendInDur;
+		}
+
+		protected TimelineClip FindTimelineClip(SpineAnimationStateClip targetClip) {
+			string[] guids = AssetDatabase.FindAssets("t:TimelineAsset");
+			foreach (string guid in guids) {
+				TimelineAsset timeline = (TimelineAsset)AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(guid), typeof(TimelineAsset));
+				foreach (var track in timeline.GetOutputTracks()) {
+					foreach (var clip in track.GetClips()) {
+						if (clip.asset.GetType() == typeof(SpineAnimationStateClip) && object.ReferenceEquals(clip.asset, targetClip)) {
+							return clip;
+						}
+					}
+				}
+			}
+			return null;
+		}
+
+	}
+}

+ 12 - 0
spine-unity/Modules/com.esotericsoftware.spine.timeline/Editor/SpineAnimationStateClipInspector.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 95642d062fbcfda4e8d9262fa715b098
+timeCreated: 1570044805
+licenseType: Free
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 29 - 8
spine-unity/Modules/com.esotericsoftware.spine.timeline/Editor/SpineAnimationStateDrawer.cs

@@ -32,30 +32,39 @@ using UnityEngine;
 using Spine;
 using Spine.Unity;
 using Spine.Unity.Playables;
+using Spine.Unity.Editor;
 
-//[CustomPropertyDrawer(typeof(SpineAnimationStateBehaviour))]
+[CustomPropertyDrawer(typeof(SpineAnimationStateBehaviour))]
 public class SpineAnimationStateDrawer : PropertyDrawer {
-	/*
+	
 	public override float GetPropertyHeight (SerializedProperty property, GUIContent label) {
 		const int fieldCount = 8;
 		return fieldCount * EditorGUIUtility.singleLineHeight;
 	}
 
 	public override void OnGUI (Rect position, SerializedProperty property, GUIContent label) {
-		SerializedProperty skeletonDataAssetProp = property.FindPropertyRelative("skeletonDataAsset");
-		SerializedProperty animationNameProp = property.FindPropertyRelative("animationName");
+		SerializedProperty animationReferenceProp = property.FindPropertyRelative("animationReference");
 		SerializedProperty loopProp = property.FindPropertyRelative("loop");
+
+		SerializedProperty customDurationProp = property.FindPropertyRelative("customDuration");
+		SerializedProperty useBlendDurationProp = property.FindPropertyRelative("useBlendDuration");
+		SerializedProperty mixDurationProp = property.FindPropertyRelative("mixDuration");
 		SerializedProperty eventProp = property.FindPropertyRelative("eventThreshold");
 		SerializedProperty attachmentProp = property.FindPropertyRelative("attachmentThreshold");
 		SerializedProperty drawOrderProp = property.FindPropertyRelative("drawOrderThreshold");
 
+		// initialize useBlendDuration parameter according to preferences
+		SerializedProperty isInitializedProp = property.FindPropertyRelative("isInitialized");
+		if (!isInitializedProp.hasMultipleDifferentValues && isInitializedProp.boolValue == false) {
+			useBlendDurationProp.boolValue = SpineEditorUtilities.Preferences.timelineUseBlendDuration;
+			isInitializedProp.boolValue = true;
+		}
+		
 		Rect singleFieldRect = new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight);
-		EditorGUI.PropertyField(singleFieldRect, skeletonDataAssetProp);
 
 		float lineHeightWithSpacing = EditorGUIUtility.singleLineHeight + 2f;
 
-		singleFieldRect.y += lineHeightWithSpacing;
-		EditorGUI.PropertyField(singleFieldRect, animationNameProp);
+		EditorGUI.PropertyField(singleFieldRect, animationReferenceProp);
 
 		singleFieldRect.y += lineHeightWithSpacing;
 		EditorGUI.PropertyField(singleFieldRect, loopProp);
@@ -65,6 +74,19 @@ public class SpineAnimationStateDrawer : PropertyDrawer {
 		singleFieldRect.y += lineHeightWithSpacing;
 		EditorGUI.LabelField(singleFieldRect, "Mixing Settings", EditorStyles.boldLabel);
 
+		singleFieldRect.y += lineHeightWithSpacing;
+		EditorGUI.PropertyField(singleFieldRect, customDurationProp);
+
+		bool greyOutCustomDurations = (!customDurationProp.hasMultipleDifferentValues &&
+										customDurationProp.boolValue == false);
+		using (new EditorGUI.DisabledGroupScope(greyOutCustomDurations)) {
+			singleFieldRect.y += lineHeightWithSpacing;
+			EditorGUI.PropertyField(singleFieldRect, useBlendDurationProp);
+
+			singleFieldRect.y += lineHeightWithSpacing;
+			EditorGUI.PropertyField(singleFieldRect, mixDurationProp);
+		}
+
 		singleFieldRect.y += lineHeightWithSpacing;
 		EditorGUI.PropertyField(singleFieldRect, eventProp);
 
@@ -74,5 +96,4 @@ public class SpineAnimationStateDrawer : PropertyDrawer {
 		singleFieldRect.y += lineHeightWithSpacing;
 		EditorGUI.PropertyField(singleFieldRect, drawOrderProp);
 	}
-	*/
 }

+ 51 - 0
spine-unity/Modules/com.esotericsoftware.spine.timeline/Editor/SpineSkeletonFlipClipInspector.cs

@@ -0,0 +1,51 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated May 1, 2019. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2019, 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.
+ *
+ * THIS SOFTWARE IS 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 THIS SOFTWARE,
+ * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+using UnityEditor;
+using Spine.Unity.Playables;
+
+namespace Spine.Unity.Editor {
+
+	[CustomEditor(typeof(SpineSkeletonFlipClip))]
+	[CanEditMultipleObjects]
+	public class SpineSkeletonFlipClipInspector : UnityEditor.Editor {
+
+		protected SerializedProperty templateProp = null;
+
+		public void OnEnable () {
+			templateProp = serializedObject.FindProperty("template");
+		}
+
+		public override void OnInspectorGUI () {
+			serializedObject.Update();
+			EditorGUILayout.PropertyField(templateProp);
+			serializedObject.ApplyModifiedProperties();
+		}
+	}
+}

+ 12 - 0
spine-unity/Modules/com.esotericsoftware.spine.timeline/Editor/SpineSkeletonFlipClipInspector.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: c6d7cdfbf1ccc0042b92586542f085a4
+timeCreated: 1570045635
+licenseType: Free
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 4 - 1
spine-unity/Modules/com.esotericsoftware.spine.timeline/Runtime/SpineAnimationState/SpineAnimationStateBehaviour.cs

@@ -44,8 +44,11 @@ namespace Spine.Unity.Playables {
 		public AnimationReferenceAsset animationReference;
 		public bool loop;
 
-		[Header("Mix Properties")]
+		// Mix Properties
 		public bool customDuration = false;
+		public bool useBlendDuration = true;
+		[SerializeField]
+		private bool isInitialized = false; // required to read preferences values from editor side.
 		public float mixDuration = 0.1f;
 
 		[Range(0, 1f)]

+ 9 - 1
spine-unity/Modules/com.esotericsoftware.spine.timeline/Runtime/SpineAnimationState/SpineAnimationStateClip.cs

@@ -37,13 +37,21 @@ namespace Spine.Unity.Playables {
 	public class SpineAnimationStateClip : PlayableAsset, ITimelineClipAsset {
 		public SpineAnimationStateBehaviour template = new SpineAnimationStateBehaviour();
 
-		public ClipCaps clipCaps { get { return ClipCaps.None; } }
+		public ClipCaps clipCaps { get { return ClipCaps.Blending | ClipCaps.ClipIn | ClipCaps.SpeedMultiplier | (template.loop ? ClipCaps.Looping : 0); } }
 
 		public override Playable CreatePlayable (PlayableGraph graph, GameObject owner) {
 			var playable = ScriptPlayable<SpineAnimationStateBehaviour>.Create(graph, template);
 			playable.GetBehaviour();
 			return playable;
 		}
+
+		public override double duration {
+			get {
+				if (template.animationReference == null)
+					return 0;
+				return template.animationReference.Animation.Duration;
+			}
+		}
 	}
 
 }

+ 11 - 10
spine-unity/Modules/com.esotericsoftware.spine.timeline/Runtime/SpineAnimationState/SpineAnimationStateMixerBehaviour.cs

@@ -70,7 +70,7 @@ namespace Spine.Unity.Playables {
 			for (int i = 0; i < inputCount; i++) {
 				float lastInputWeight = lastInputWeights[i];
 				float inputWeight = playable.GetInputWeight(i);
-				bool trackStarted = inputWeight > lastInputWeight;
+				bool trackStarted = lastInputWeight == 0 && inputWeight > 0;
 				lastInputWeights[i] = inputWeight;
 
 				if (trackStarted) {
@@ -84,9 +84,10 @@ namespace Spine.Unity.Playables {
 						if (clipData.animationReference.Animation != null) {
 							Spine.TrackEntry trackEntry = state.SetAnimation(trackIndex, clipData.animationReference.Animation, clipData.loop);
 
-							//trackEntry.TrackTime = (float)inputPlayable.GetTime(); // More accurate time-start?
 							trackEntry.EventThreshold = clipData.eventThreshold;
 							trackEntry.DrawOrderThreshold = clipData.drawOrderThreshold;
+							trackEntry.TrackTime = (float)inputPlayable.GetTime() * (float)inputPlayable.GetSpeed();
+							trackEntry.TimeScale = (float)inputPlayable.GetSpeed();
 							trackEntry.AttachmentThreshold = clipData.attachmentThreshold;
 
 							if (clipData.customDuration)
@@ -111,15 +112,15 @@ namespace Spine.Unity.Playables {
 			if (spineComponent == null) return;
 
 			int inputCount = playable.GetInputCount();
-			int lastOneWeight = -1;
+			int lastNonZeroWeightTrack = -1;
 
 			for (int i = 0; i < inputCount; i++) {
 				float inputWeight = playable.GetInputWeight(i);
-				if (inputWeight >= 1) lastOneWeight = i;
+				if (inputWeight > 0) lastNonZeroWeightTrack = i;
 			}
 
-			if (lastOneWeight != -1) {
-				ScriptPlayable<SpineAnimationStateBehaviour> inputPlayableClip = (ScriptPlayable<SpineAnimationStateBehaviour>)playable.GetInput(lastOneWeight);
+			if (lastNonZeroWeightTrack != -1) {
+				ScriptPlayable<SpineAnimationStateBehaviour> inputPlayableClip = (ScriptPlayable<SpineAnimationStateBehaviour>)playable.GetInput(lastNonZeroWeightTrack);
 				SpineAnimationStateBehaviour clipData = inputPlayableClip.GetBehaviour();
 
 				var skeleton = spineComponent.Skeleton;
@@ -133,16 +134,16 @@ namespace Spine.Unity.Playables {
 				Animation fromAnimation = null;
 				float fromClipTime = 0;
 				bool fromClipLoop = false;
-				if (lastOneWeight != 0 && inputCount > 1) {
-					var fromClip = (ScriptPlayable<SpineAnimationStateBehaviour>)playable.GetInput(lastOneWeight - 1);
+				if (lastNonZeroWeightTrack != 0 && inputCount > 1) {
+					var fromClip = (ScriptPlayable<SpineAnimationStateBehaviour>)playable.GetInput(lastNonZeroWeightTrack - 1);
 					var fromClipData = fromClip.GetBehaviour();
 					fromAnimation = fromClipData.animationReference != null ? fromClipData.animationReference.Animation : null;
-					fromClipTime = (float)fromClip.GetTime();
+					fromClipTime = (float)fromClip.GetTime() * (float)fromClip.GetSpeed();
 					fromClipLoop = fromClipData.loop;
 				}
 
 				Animation toAnimation = clipData.animationReference != null ? clipData.animationReference.Animation : null;
-				float toClipTime = (float)inputPlayableClip.GetTime();
+				float toClipTime = (float)inputPlayableClip.GetTime() * (float)inputPlayableClip.GetSpeed();
 				float mixDuration = clipData.mixDuration;
 
 				if (!clipData.customDuration && fromAnimation != null && toAnimation != null) {