瀏覽代碼

[unity] Added Root Motion support for `SkeletonAnimation`, `SkeletonMecanim` and `SkeletonGraphic`. See #1417.

Harald Csaszar 5 年之前
父節點
當前提交
528ab0cff2
共有 19 個文件被更改,包括 1020 次插入54 次删除
  1. 1 0
      CHANGELOG.md
  2. 2 0
      spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonAnimationInspector.cs
  3. 2 0
      spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonGraphicInspector.cs
  4. 101 3
      spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonMecanimInspector.cs
  5. 81 0
      spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonMecanimRootMotionInspector.cs
  6. 12 0
      spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonMecanimRootMotionInspector.cs.meta
  7. 42 1
      spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonRendererInspector.cs
  8. 92 0
      spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonRootMotionBaseInspector.cs
  9. 12 0
      spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonRootMotionBaseInspector.cs.meta
  10. 79 0
      spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonRootMotionInspector.cs
  11. 12 0
      spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonRootMotionInspector.cs.meta
  12. 9 0
      spine-unity/Assets/Spine/Runtime/spine-unity/Components/RootMotion.meta
  13. 100 0
      spine-unity/Assets/Spine/Runtime/spine-unity/Components/RootMotion/SkeletonMecanimRootMotion.cs
  14. 12 0
      spine-unity/Assets/Spine/Runtime/spine-unity/Components/RootMotion/SkeletonMecanimRootMotion.cs.meta
  15. 143 0
      spine-unity/Assets/Spine/Runtime/spine-unity/Components/RootMotion/SkeletonRootMotion.cs
  16. 12 0
      spine-unity/Assets/Spine/Runtime/spine-unity/Components/RootMotion/SkeletonRootMotion.cs.meta
  17. 184 0
      spine-unity/Assets/Spine/Runtime/spine-unity/Components/RootMotion/SkeletonRootMotionBase.cs
  18. 12 0
      spine-unity/Assets/Spine/Runtime/spine-unity/Components/RootMotion/SkeletonRootMotionBase.cs.meta
  19. 112 50
      spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonMecanim.cs

+ 1 - 0
CHANGELOG.md

@@ -231,6 +231,7 @@
   * Added support for **multiple atlas textures at `SkeletonGraphic`**. You can enable this feature by enabling the parameter `Multiple CanvasRenders` in the `Advanced` section of the `SkeletonGraphic` Inspector. This automatically creates the required number of child `CanvasRenderer` GameObjects for each required draw call (submesh).
   * Added support for **Render Separator Slots** at `SkeletonGraphic`. Render separation can be enabled directly in the `Advanced` section of the `SkeletonGraphic` Inspector, it does not require any additional components (like `SkeletonRenderSeparator` or `SkeletonPartsRenderer` for `SkeletonRenderer` components). When enabled, additional separator GameObjects will be created automatically for each separation part, and `CanvasRenderer` GameObjects re-parented to them accordingly. The separator GameObjects can be moved around and re-parented in the hierarchy according to your requirements to achieve the desired draw order within your `Canvas`. A usage example can be found in the updated `Spine Examples/Other Examples/SkeletonRenderSeparator` scene.
   * Added `SkeletonGraphicCustomMaterials` component, providing functionality to override materials and textures of a `SkeletonGraphic`, similar to `SkeletonRendererCustomMaterials`. Note: overriding materials or textures per slot is not provided due to structural limitations.
+  * Added **Root Motion support** for `SkeletonAnimation`, `SkeletonMecanim` and `SkeletonGraphic` via new components `SkeletonRootMotion` and `SkeletonMecanimRootMotion`. The `SkeletonAnimation` and `SkeletonGraphic` component Inspector now provides a line `Root Motion` with `Add Component` and `Remove Component` buttons to add/remove the new `SkeletonRootMotion` component to your GameObject. The `SkeletonMecanim` Inspector detects whether root motion is enabled at the `Animator` component and adds a `SkeletonMecanimRootMotion` component automatically.
 
 * **Changes of default values**
   * `SkeletonMecanim`'s `Layer Mix Mode` now defaults to `MixMode.MixNext` instead of `MixMode.MixAlways`.

+ 2 - 0
spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonAnimationInspector.cs

@@ -72,7 +72,9 @@ namespace Spine.Unity.Editor {
 				var component = o as SkeletonAnimation;
 				component.timeScale = Mathf.Max(component.timeScale, 0);
 			}
+
 			EditorGUILayout.Space();
+			SkeletonRootMotionParameter();
 
 			if (!isInspectingPrefab) {
 				if (requireRepaint) {

+ 2 - 0
spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonGraphicInspector.cs

@@ -234,6 +234,8 @@ namespace Spine.Unity.Editor {
 			EditorGUILayout.Space();
 			EditorGUILayout.PropertyField(freeze);
 			EditorGUILayout.Space();
+			SkeletonRendererInspector.SkeletonRootMotionParameter(targets);
+			EditorGUILayout.Space();
 			EditorGUILayout.LabelField("UI", EditorStyles.boldLabel);
 			EditorGUILayout.PropertyField(raycastTarget);
 

+ 101 - 3
spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonMecanimInspector.cs

@@ -30,21 +30,119 @@
 // Contributed by: Mitch Thompson
 
 using UnityEditor;
+using UnityEngine;
 
 namespace Spine.Unity.Editor {
 	[CustomEditor(typeof(SkeletonMecanim))]
 	[CanEditMultipleObjects]
 	public class SkeletonMecanimInspector : SkeletonRendererInspector {
-		protected SerializedProperty mecanimTranslator;
+		public static bool mecanimSettingsFoldout;
+
+		protected SerializedProperty autoReset;
+		protected SerializedProperty layerMixModes;
+		protected SerializedProperty layerBlendModes;
 
 		protected override void OnEnable () {
 			base.OnEnable();
-			mecanimTranslator = serializedObject.FindProperty("translator");
+			SerializedProperty mecanimTranslator = serializedObject.FindProperty("translator");
+			autoReset = mecanimTranslator.FindPropertyRelative("autoReset");
+			layerMixModes = mecanimTranslator.FindPropertyRelative("layerMixModes");
+			layerBlendModes = mecanimTranslator.FindPropertyRelative("layerBlendModes");
 		}
 
 		protected override void DrawInspectorGUI (bool multi) {
+
+			AddRootMotionComponentIfEnabled();
+
 			base.DrawInspectorGUI(multi);
-			EditorGUILayout.PropertyField(mecanimTranslator, true);
+
+			using (new SpineInspectorUtility.BoxScope()) {
+				mecanimSettingsFoldout = EditorGUILayout.Foldout(mecanimSettingsFoldout, "Mecanim Translator");
+				if (mecanimSettingsFoldout) {
+					EditorGUILayout.PropertyField(autoReset, new GUIContent("Auto Reset",
+						"When set to true, the skeleton state is mixed out to setup-" +
+						"pose when an animation finishes, according to the " +
+						"animation's keyed items."));
+
+					EditorGUILayout.Space();
+					DrawLayerSettings();
+					EditorGUILayout.Space();
+				}
+			}
+		}
+
+		protected void AddRootMotionComponentIfEnabled () {
+			foreach (var t in targets) {
+				var component = t as Component;
+				var animator = component.GetComponent<Animator>();
+				if (animator != null && animator.applyRootMotion) {
+					if (component.GetComponent<SkeletonMecanimRootMotion>() == null) {
+						component.gameObject.AddComponent<SkeletonMecanimRootMotion>();
+					}
+				}
+			}
+		}
+
+		protected void DrawLayerSettings () {
+			string[] layerNames = GetLayerNames();
+			float widthLayerColumn = 140;
+			float widthMixColumn = 84;
+
+			using (new GUILayout.HorizontalScope()) {
+				var rect = GUILayoutUtility.GetRect(EditorGUIUtility.currentViewWidth, EditorGUIUtility.singleLineHeight);
+				rect.width = widthLayerColumn;
+				EditorGUI.LabelField(rect, SpineInspectorUtility.TempContent("Mecanim Layer"), EditorStyles.boldLabel);
+
+				var savedIndent = EditorGUI.indentLevel;
+				EditorGUI.indentLevel = 0;
+
+				rect.position += new Vector2(rect.width, 0);
+				rect.width = widthMixColumn;
+				EditorGUI.LabelField(rect, SpineInspectorUtility.TempContent("Mix Mode"), EditorStyles.boldLabel);
+
+				EditorGUI.indentLevel = savedIndent;
+			}
+
+			using (new SpineInspectorUtility.IndentScope()) {
+				int layerCount = layerMixModes.arraySize;
+				for (int i = 0; i < layerCount; ++i) {
+					using (new GUILayout.HorizontalScope()) {
+						string layerName = i < layerNames.Length ? layerNames[i] : ("Layer " + i);
+
+						var rect = GUILayoutUtility.GetRect(EditorGUIUtility.currentViewWidth, EditorGUIUtility.singleLineHeight);
+						rect.width = widthLayerColumn;
+						EditorGUI.PrefixLabel(rect, SpineInspectorUtility.TempContent(layerName));
+
+						var savedIndent = EditorGUI.indentLevel;
+						EditorGUI.indentLevel = 0;
+
+						var mixMode = layerMixModes.GetArrayElementAtIndex(i);
+						var blendMode = layerBlendModes.GetArrayElementAtIndex(i);
+						rect.position += new Vector2(rect.width, 0);
+						rect.width = widthMixColumn;
+						EditorGUI.PropertyField(rect, mixMode, GUIContent.none);
+
+						EditorGUI.indentLevel = savedIndent;
+					}
+				}
+			}
+		}
+
+		protected string[] GetLayerNames () {
+			int maxLayerCount = 0;
+			int maxIndex = 0;
+			for (int i = 0; i < targets.Length; ++i) {
+				var skeletonMecanim = ((SkeletonMecanim)targets[i]);
+				int count = skeletonMecanim.Translator.MecanimLayerCount;
+				if (count > maxLayerCount) {
+					maxLayerCount = count;
+					maxIndex = i;
+				}
+			}
+			if (maxLayerCount == 0)
+				return new string[0];
+			var skeletonMecanimMaxLayers = ((SkeletonMecanim)targets[maxIndex]);
+			return skeletonMecanimMaxLayers.Translator.MecanimLayerNames;
 		}
 	}
 }

+ 81 - 0
spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonMecanimRootMotionInspector.cs

@@ -0,0 +1,81 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, 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 UnityEditor;
+using UnityEngine;
+
+namespace Spine.Unity.Editor {
+	[CustomEditor(typeof(SkeletonMecanimRootMotion))]
+	[CanEditMultipleObjects]
+	public class SkeletonMecanimRootMotionInspector : SkeletonRootMotionBaseInspector {
+		protected SerializedProperty mecanimLayerFlags;
+
+		protected GUIContent mecanimLayersLabel;
+
+		protected override void OnEnable () {
+			base.OnEnable();
+			mecanimLayerFlags = serializedObject.FindProperty("mecanimLayerFlags");
+
+			mecanimLayersLabel = new UnityEngine.GUIContent("Mecanim Layers", "Mecanim layers to apply root motion at. Defaults to the first Mecanim layer.");
+		}
+
+		override public void OnInspectorGUI () {
+
+			base.MainPropertyFields();
+			MecanimLayerMaskPropertyField();
+
+			base.OptionalPropertyFields();
+			serializedObject.ApplyModifiedProperties();
+		}
+
+		protected string[] GetLayerNames () {
+			int maxLayerCount = 0;
+			int maxIndex = 0;
+			for (int i = 0; i < targets.Length; ++i) {
+				var skeletonMecanim = ((SkeletonMecanimRootMotion)targets[i]).SkeletonMecanim;
+				int count = skeletonMecanim.Translator.MecanimLayerCount;
+				if (count > maxLayerCount) {
+					maxLayerCount = count;
+					maxIndex = i;
+				}
+			}
+			if (maxLayerCount == 0)
+				return new string[0];
+			var skeletonMecanimMaxLayers = ((SkeletonMecanimRootMotion)targets[maxIndex]).SkeletonMecanim;
+			return skeletonMecanimMaxLayers.Translator.MecanimLayerNames;
+		}
+
+		protected void MecanimLayerMaskPropertyField () {
+			string[] layerNames = GetLayerNames();
+			if (layerNames.Length > 0)
+				mecanimLayerFlags.intValue = EditorGUILayout.MaskField(
+					mecanimLayersLabel, mecanimLayerFlags.intValue, layerNames);
+		}
+	}
+}

+ 12 - 0
spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonMecanimRootMotionInspector.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 4613924c50d66cf458f0db803776dd2f
+timeCreated: 1593175106
+licenseType: Pro
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 42 - 1
spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonRendererInspector.cs

@@ -106,7 +106,6 @@ namespace Spine.Unity.Editor {
 #else
 			isInspectingPrefab = (PrefabUtility.GetPrefabType(target) == PrefabType.Prefab);
 #endif
-
 			SpineEditorUtilities.ConfirmInitialization();
 
 			// Labels
@@ -398,6 +397,48 @@ namespace Spine.Unity.Editor {
 			}
 		}
 
+		protected void SkeletonRootMotionParameter() {
+			SkeletonRootMotionParameter(targets);
+		}
+
+		public static void SkeletonRootMotionParameter(Object[] targets) {
+			int rootMotionComponentCount = 0;
+			foreach (var t in targets) {
+				var component = t as Component;
+				if (component.GetComponent<SkeletonRootMotion>() != null) {
+					++rootMotionComponentCount;
+				}
+			}
+			bool allHaveRootMotion = rootMotionComponentCount == targets.Length;
+			bool anyHaveRootMotion = rootMotionComponentCount > 0;
+
+			using (new GUILayout.HorizontalScope()) {
+				EditorGUILayout.PrefixLabel("Root Motion");
+
+				if (!allHaveRootMotion) {
+					if (GUILayout.Button(SpineInspectorUtility.TempContent("Add Component", Icons.constraintTransform), GUILayout.MaxWidth(130), GUILayout.Height(18))) {
+						foreach (var t in targets) {
+							var component = t as Component;
+							if (component.GetComponent<SkeletonRootMotion>() == null) {
+								component.gameObject.AddComponent<SkeletonRootMotion>();
+							}
+						}
+					}
+				}
+				if (anyHaveRootMotion) {
+					if (GUILayout.Button(SpineInspectorUtility.TempContent("Remove Component", Icons.constraintTransform), GUILayout.MaxWidth(140), GUILayout.Height(18))) {
+						foreach (var t in targets) {
+							var component = t as Component;
+							var rootMotionComponent = component.GetComponent<SkeletonRootMotion>();
+							if (rootMotionComponent  != null) {
+								DestroyImmediate(rootMotionComponent);
+							}
+						}
+					}
+				}
+			}
+		}
+
 		public static void SetSeparatorSlotNames (SkeletonRenderer skeletonRenderer, string[] newSlotNames) {
 			var field = SpineInspectorUtility.GetNonPublicField(typeof(SkeletonRenderer), SeparatorSlotNamesFieldName);
 			field.SetValue(skeletonRenderer, newSlotNames);

+ 92 - 0
spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonRootMotionBaseInspector.cs

@@ -0,0 +1,92 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, 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 UnityEditor;
+using UnityEngine;
+
+namespace Spine.Unity.Editor {
+	[CustomEditor(typeof(SkeletonRootMotionBase))]
+	[CanEditMultipleObjects]
+	public class SkeletonRootMotionBaseInspector : UnityEditor.Editor {
+		protected SerializedProperty rootMotionBoneName;
+		protected SerializedProperty transformPositionX;
+		protected SerializedProperty transformPositionY;
+		protected SerializedProperty rigidBody2D;
+		protected SerializedProperty rigidBody;
+
+		protected GUIContent rootMotionBoneNameLabel;
+		protected GUIContent transformPositionXLabel;
+		protected GUIContent transformPositionYLabel;
+		protected GUIContent rigidBody2DLabel;
+		protected GUIContent rigidBodyLabel;
+
+		protected virtual void OnEnable () {
+
+			rootMotionBoneName = serializedObject.FindProperty("rootMotionBoneName");
+			transformPositionX = serializedObject.FindProperty("transformPositionX");
+			transformPositionY = serializedObject.FindProperty("transformPositionY");
+			rigidBody2D = serializedObject.FindProperty("rigidBody2D");
+			rigidBody = serializedObject.FindProperty("rigidBody");
+
+			rootMotionBoneNameLabel = new UnityEngine.GUIContent("Root Motion Bone", "The bone to take the motion from.");
+			transformPositionXLabel = new UnityEngine.GUIContent("X", "Root transform position (X)");
+			transformPositionYLabel = new UnityEngine.GUIContent("Y", "Use the Y-movement of the bone.");
+			rigidBody2DLabel = new UnityEngine.GUIContent("Rigidbody2D",
+				"Optional Rigidbody2D: Assign a Rigidbody2D here if you want " +
+				" to apply the root motion to the rigidbody instead of the Transform." +
+				"\n\n" +
+				"Note that animation and physics updates are not always in sync." +
+				"Some jitter may result at certain framerates.");
+			rigidBodyLabel = new UnityEngine.GUIContent("Rigidbody",
+				"Optional Rigidbody: Assign a Rigidbody here if you want " +
+				" to apply the root motion to the rigidbody instead of the Transform." +
+				"\n\n" +
+				"Note that animation and physics updates are not always in sync." +
+				"Some jitter may result at certain framerates.");
+		}
+
+		public override void OnInspectorGUI () {
+			MainPropertyFields();
+			OptionalPropertyFields();
+			serializedObject.ApplyModifiedProperties();
+		}
+
+		protected virtual void MainPropertyFields () {
+			EditorGUILayout.PropertyField(rootMotionBoneName, rootMotionBoneNameLabel);
+			EditorGUILayout.PropertyField(transformPositionX, transformPositionXLabel);
+			EditorGUILayout.PropertyField(transformPositionY, transformPositionYLabel);
+		}
+
+		protected virtual void OptionalPropertyFields () {
+			//EditorGUILayout.LabelField("Optional", EditorStyles.boldLabel);
+			EditorGUILayout.PropertyField(rigidBody2D, rigidBody2DLabel);
+			EditorGUILayout.PropertyField(rigidBody, rigidBodyLabel);
+		}
+	}
+}

+ 12 - 0
spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonRootMotionBaseInspector.cs.meta

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

+ 79 - 0
spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonRootMotionInspector.cs

@@ -0,0 +1,79 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, 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 UnityEditor;
+using UnityEngine;
+
+namespace Spine.Unity.Editor {
+	[CustomEditor(typeof(SkeletonRootMotion))]
+	[CanEditMultipleObjects]
+	public class SkeletonRootMotionInspector : SkeletonRootMotionBaseInspector {
+		protected SerializedProperty animationTrackFlags;
+		protected GUIContent animationTrackFlagsLabel;
+
+		string[] TrackNames;
+
+		protected override void OnEnable () {
+			base.OnEnable();
+
+			animationTrackFlags = serializedObject.FindProperty("animationTrackFlags");
+			animationTrackFlagsLabel = new UnityEngine.GUIContent("Animation Tracks",
+				"Animation tracks to apply root motion at. Defaults to the first" +
+				" animation track (index 0).");
+		}
+
+		override public void OnInspectorGUI () {
+
+			base.MainPropertyFields();
+			AnimationTracksPropertyField();
+
+			base.OptionalPropertyFields();
+			serializedObject.ApplyModifiedProperties();
+		}
+
+		protected void AnimationTracksPropertyField () {
+
+			if (TrackNames == null) {
+				InitTrackNames();
+
+			}
+
+			animationTrackFlags.intValue = EditorGUILayout.MaskField(
+				animationTrackFlagsLabel, animationTrackFlags.intValue, TrackNames);
+		}
+
+		protected void InitTrackNames () {
+			int numEntries = 32;
+			TrackNames = new string[numEntries];
+			for (int i = 0; i < numEntries; ++i) {
+				TrackNames[i] = string.Format("Track {0}", i);
+			}
+		}
+	}
+}

+ 12 - 0
spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonRootMotionInspector.cs.meta

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

+ 9 - 0
spine-unity/Assets/Spine/Runtime/spine-unity/Components/RootMotion.meta

@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: 2b957aa69dae9f948bacdeec549d28ea
+folderAsset: yes
+timeCreated: 1593173800
+licenseType: Pro
+DefaultImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 100 - 0
spine-unity/Assets/Spine/Runtime/spine-unity/Components/RootMotion/SkeletonMecanimRootMotion.cs

@@ -0,0 +1,100 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, 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 UnityEngine;
+using System.Collections.Generic;
+using Spine.Unity.AnimationTools;
+
+namespace Spine.Unity {
+
+	/// <summary>
+	/// Add this component to a SkeletonMecanim GameObject
+	/// to turn motion of a selected root bone into Transform or RigidBody motion.
+	/// Local bone translation movement is used as motion.
+	/// All top-level bones of the skeleton are moved to compensate the root
+	/// motion bone location, keeping the distance relationship between bones intact.
+	/// </summary>
+	/// <remarks>
+	/// Only compatible with <c>SkeletonMecanim</c>.
+	/// For <c>SkeletonAnimation</c> or <c>SkeletonGraphic</c> please use
+	/// <see cref="SkeletonRootMotion">SkeletonRootMotion</see> instead.
+	/// </remarks>
+	public class SkeletonMecanimRootMotion : SkeletonRootMotionBase {
+		#region Inspector
+		const int DefaultMecanimLayerFlags = -1;
+		public int mecanimLayerFlags = DefaultMecanimLayerFlags;
+		#endregion
+
+		protected Vector2 movementDelta;
+
+		SkeletonMecanim skeletonMecanim;
+		public SkeletonMecanim SkeletonMecanim {
+			get {
+				return skeletonMecanim ? skeletonMecanim : skeletonMecanim = GetComponent<SkeletonMecanim>();
+			}
+		}
+
+		protected override void Reset () {
+			base.Reset();
+			mecanimLayerFlags = DefaultMecanimLayerFlags;
+		}
+
+		protected override void Start () {
+			base.Start();
+			skeletonMecanim = GetComponent<SkeletonMecanim>();
+			if (skeletonMecanim) {
+				skeletonMecanim.Translator.OnClipApplied -= OnClipApplied;
+				skeletonMecanim.Translator.OnClipApplied += OnClipApplied;
+			}
+		}
+
+		void OnClipApplied(Spine.Animation clip, int layerIndex, float weight,
+				float time, float lastTime, bool playsBackward) {
+
+			if (((mecanimLayerFlags & 1<<layerIndex) == 0) || weight == 0)
+				return;
+
+			var timeline = clip.FindTranslateTimelineForBone(rootMotionBoneIndex);
+			if (timeline != null) {
+				if (!playsBackward)
+					movementDelta += weight * GetTimelineMovementDelta(lastTime, time, timeline, clip);
+				else
+					movementDelta -= weight * GetTimelineMovementDelta(time, lastTime, timeline, clip);
+			}
+		}
+
+		protected override Vector2 CalculateAnimationsMovementDelta () {
+			// Note: movement delta is not gather after animation but
+			// in OnClipApplied after every applied animation.
+			Vector2 result = movementDelta;
+			movementDelta = Vector2.zero;
+			return result;
+		}
+	}
+}

+ 12 - 0
spine-unity/Assets/Spine/Runtime/spine-unity/Components/RootMotion/SkeletonMecanimRootMotion.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 95813afe390494344a6ce2cbc8bfb7d1
+timeCreated: 1592849332
+licenseType: Pro
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 143 - 0
spine-unity/Assets/Spine/Runtime/spine-unity/Components/RootMotion/SkeletonRootMotion.cs

@@ -0,0 +1,143 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, 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 UnityEngine;
+using System.Collections.Generic;
+using Spine.Unity.AnimationTools;
+
+namespace Spine.Unity {
+
+	/// <summary>
+	/// Add this component to a SkeletonAnimation or SkeletonGraphic GameObject
+	/// to turn motion of a selected root bone into Transform or RigidBody motion.
+	/// Local bone translation movement is used as motion.
+	/// All top-level bones of the skeleton are moved to compensate the root
+	/// motion bone location, keeping the distance relationship between bones intact.
+	/// </summary>
+	/// <remarks>
+	/// Only compatible with SkeletonAnimation (or other components that implement
+	/// ISkeletonComponent, ISkeletonAnimation and IAnimationStateComponent).
+	/// For <c>SkeletonMecanim</c> please use
+	/// <see cref="SkeletonMecanimRootMotion">SkeletonMecanimRootMotion</see> instead.
+	/// </remarks>
+	public class SkeletonRootMotion : SkeletonRootMotionBase {
+		#region Inspector
+		const int DefaultAnimationTrackFlags = -1;
+		public int animationTrackFlags = DefaultAnimationTrackFlags;
+		#endregion
+
+		AnimationState animationState;
+		Canvas canvas;
+
+		protected override float AdditionalScale {
+			get {
+				return canvas ? canvas.referencePixelsPerUnit: 1.0f;
+			}
+		}
+
+		protected override void Reset () {
+			base.Reset();
+			animationTrackFlags = DefaultAnimationTrackFlags;
+		}
+
+		protected override void Start () {
+			base.Start();
+			var animstateComponent = skeletonComponent as IAnimationStateComponent;
+			this.animationState = (animstateComponent != null) ? animstateComponent.AnimationState : null;
+
+			if (this.GetComponent<CanvasRenderer>() != null) {
+				canvas = this.GetComponentInParent<Canvas>();
+			}
+		}
+
+		protected override Vector2 CalculateAnimationsMovementDelta () {
+			Vector2 localDelta = Vector2.zero;
+			int trackCount = animationState.Tracks.Count;
+
+			for (int trackIndex = 0; trackIndex < trackCount; ++trackIndex) {
+				// note: animationTrackFlags != -1 below covers trackIndex >= 32,
+				// with -1 corresponding to entry "everything" of the dropdown list.
+				if (animationTrackFlags != -1 && (animationTrackFlags & 1 << trackIndex) == 0)
+					continue;
+
+				TrackEntry track = animationState.GetCurrent(trackIndex);
+				TrackEntry next = null;
+				while (track != null) {
+					var animation = track.Animation;
+					var timeline = animation.FindTranslateTimelineForBone(rootMotionBoneIndex);
+					if (timeline != null) {
+						var currentDelta = GetTrackMovementDelta(track, timeline, animation, next);
+						localDelta += currentDelta;
+					}
+					// Traverse mixingFrom chain.
+					next = track;
+					track = track.mixingFrom;
+				}
+			}
+			return localDelta;
+		}
+
+		Vector2 GetTrackMovementDelta (TrackEntry track, TranslateTimeline timeline,
+			Animation animation, TrackEntry next) {
+
+			float start = track.animationLast;
+			float end = track.AnimationTime;
+			Vector2 currentDelta = GetTimelineMovementDelta(start, end, timeline, animation);
+
+			ApplyMixAlphaToDelta(ref currentDelta, next, track);
+			return currentDelta;
+		}
+
+		void ApplyMixAlphaToDelta (ref Vector2 currentDelta, TrackEntry next, TrackEntry track) {
+			// Apply mix alpha to the delta position (based on AnimationState.cs).
+			float mix;
+			if (next != null) {
+				if (next.mixDuration == 0) { // Single frame mix to undo mixingFrom changes.
+					mix = 1;
+				}
+				else {
+					mix = next.mixTime / next.mixDuration;
+					if (mix > 1) mix = 1;
+				}
+				float mixAndAlpha = track.alpha * next.interruptAlpha * (1 - mix);
+				currentDelta *= mixAndAlpha;
+			}
+			else {
+				if (track.mixDuration == 0) {
+					mix = 1;
+				}
+				else {
+					mix = track.alpha * (track.mixTime / track.mixDuration);
+					if (mix > 1) mix = 1;
+				}
+				currentDelta *= mix;
+			}
+		}
+	}
+}

+ 12 - 0
spine-unity/Assets/Spine/Runtime/spine-unity/Components/RootMotion/SkeletonRootMotion.cs.meta

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

+ 184 - 0
spine-unity/Assets/Spine/Runtime/spine-unity/Components/RootMotion/SkeletonRootMotionBase.cs

@@ -0,0 +1,184 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, 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 UnityEngine;
+using System.Collections.Generic;
+using Spine.Unity.AnimationTools;
+
+namespace Spine.Unity {
+
+	/// <summary>
+	/// Base class for skeleton root motion components.
+	/// </summary>
+	abstract public class SkeletonRootMotionBase : MonoBehaviour {
+		#region Inspector
+
+		[SpineBone]
+		[SerializeField]
+		protected string rootMotionBoneName = "root";
+		public bool transformPositionX = true;
+		public bool transformPositionY = true;
+
+		[Header("Optional")]
+		public Rigidbody2D rigidBody2D;
+		public Rigidbody rigidBody;
+
+		public bool UsesRigidbody {
+			get { return rigidBody != null || rigidBody2D != null; }
+		}
+		#endregion
+
+		protected ISkeletonComponent skeletonComponent;
+		protected Bone rootMotionBone;
+		protected int rootMotionBoneIndex;
+		protected List<Bone> topLevelBones = new List<Bone>();
+		protected Vector2 rigidbodyDisplacement;
+
+		protected virtual void Reset () {
+			FindRigidbodyComponent();
+		}
+
+		protected virtual void Start () {
+			skeletonComponent = GetComponent<ISkeletonComponent>();
+			GatherTopLevelBones();
+			SetRootMotionBone(rootMotionBoneName);
+
+			var skeletonAnimation = skeletonComponent as ISkeletonAnimation;
+			if (skeletonAnimation != null)
+				skeletonAnimation.UpdateLocal += HandleUpdateLocal;
+		}
+
+		abstract protected Vector2 CalculateAnimationsMovementDelta ();
+
+		protected virtual float AdditionalScale { get { return 1.0f; } }
+
+		protected Vector2 GetTimelineMovementDelta (float startTime, float endTime,
+			TranslateTimeline timeline, Animation animation) {
+
+			Vector2 currentDelta;
+			if (startTime > endTime) // Looped
+				currentDelta = (timeline.Evaluate(animation.duration) - timeline.Evaluate(startTime))
+					+ (timeline.Evaluate(endTime) - timeline.Evaluate(0));
+			else if (startTime != endTime) // Non-looped
+				currentDelta = timeline.Evaluate(endTime) - timeline.Evaluate(startTime);
+			else
+				currentDelta = Vector2.zero;
+			return currentDelta;
+		}
+
+		void GatherTopLevelBones () {
+			topLevelBones.Clear();
+			var skeleton = skeletonComponent.Skeleton;
+			foreach (var bone in skeleton.Bones) {
+				if (bone.Parent == null)
+					topLevelBones.Add(bone);
+			}
+		}
+
+		public void SetRootMotionBone (string name) {
+			var skeleton = skeletonComponent.Skeleton;
+			int index = skeleton.FindBoneIndex(name);
+			if (index >= 0) {
+				this.rootMotionBoneIndex = index;
+				this.rootMotionBone = skeleton.bones.Items[index];
+			}
+			else {
+				Debug.Log("Bone named \"" + name + "\" could not be found.");
+				this.rootMotionBoneIndex = 0;
+				this.rootMotionBone = skeleton.RootBone;
+			}
+		}
+
+		void HandleUpdateLocal (ISkeletonAnimation animatedSkeletonComponent) {
+			if (!this.isActiveAndEnabled)
+				return; // Root motion is only applied when component is enabled.
+
+			var movementDelta = CalculateAnimationsMovementDelta();
+			AdjustMovementDeltaToConfiguration(ref movementDelta, animatedSkeletonComponent.Skeleton);
+			ApplyRootMotion(movementDelta);
+		}
+
+		void AdjustMovementDeltaToConfiguration (ref Vector2 localDelta, Skeleton skeleton) {
+			if (skeleton.ScaleX < 0) localDelta.x = -localDelta.x;
+			if (skeleton.ScaleY < 0) localDelta.y = -localDelta.y;
+			if (!transformPositionX) localDelta.x = 0f;
+			if (!transformPositionY) localDelta.y = 0f;
+		}
+
+		void ApplyRootMotion (Vector2 localDelta) {
+			localDelta *= AdditionalScale;
+			// Apply root motion to Transform or RigidBody;
+			if (UsesRigidbody) {
+				rigidbodyDisplacement += (Vector2)transform.TransformVector(localDelta);
+				// Accumulated displacement is applied on the next Physics update (FixedUpdate)
+			}
+			else {
+
+				transform.position += transform.TransformVector(localDelta);
+			}
+
+			// Move top level bones in opposite direction of the root motion bone
+			foreach (var topLevelBone in topLevelBones) {
+				if (transformPositionX) topLevelBone.x -= rootMotionBone.x;
+				if (transformPositionY) topLevelBone.y -= rootMotionBone.y;
+			}
+		}
+
+		protected virtual void FixedUpdate () {
+			if (!this.isActiveAndEnabled)
+				return; // Root motion is only applied when component is enabled.
+
+			if(rigidBody2D != null) {
+				rigidBody2D.MovePosition(new Vector2(transform.position.x, transform.position.y)
+					+ rigidbodyDisplacement);
+			}
+			if (rigidBody != null) {
+				rigidBody.MovePosition(transform.position
+					+ new Vector3(rigidbodyDisplacement.x, rigidbodyDisplacement.y, 0));
+			}
+			rigidbodyDisplacement = Vector2.zero;
+		}
+
+		protected virtual void OnDisable () {
+			rigidbodyDisplacement = Vector2.zero;
+		}
+
+		protected void FindRigidbodyComponent () {
+			rigidBody2D = this.GetComponent<Rigidbody2D>();
+			if (!rigidBody2D)
+				rigidBody = this.GetComponent<Rigidbody>();
+
+			if (!rigidBody2D && !rigidBody) {
+				rigidBody2D = this.GetComponentInParent<Rigidbody2D>();
+				if (!rigidBody2D)
+					rigidBody = this.GetComponentInParent<Rigidbody>();
+			}
+		}
+	}
+}

+ 12 - 0
spine-unity/Assets/Spine/Runtime/spine-unity/Components/RootMotion/SkeletonRootMotionBase.cs.meta

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

+ 112 - 50
spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonMecanim.cs

@@ -129,6 +129,12 @@ namespace Spine.Unity {
 			public MixBlend[] layerBlendModes = new MixBlend[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 }
 
 			readonly Dictionary<int, Spine.Animation> animationTable = new Dictionary<int, Spine.Animation>(IntEqualityComparer.Instance);
@@ -157,6 +163,26 @@ namespace Spine.Unity {
 			Animator animator;
 			public Animator Animator { get { return this.animator; } }
 
+			public int MecanimLayerCount {
+				get {
+					if (!animator)
+						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;
 
@@ -171,10 +197,71 @@ namespace Spine.Unity {
 				ClearClipInfosForLayers();
 			}
 
+			private bool ApplyAnimation (Skeleton skeleton, AnimatorClipInfo info, AnimatorStateInfo stateInfo,
+										int layerIndex, float layerWeight, MixBlend layerBlendMode, bool useWeight1 = false) {
+				float weight = info.weight * layerWeight;
+				if (weight == 0)
+					return false;
+
+				var clip = GetAnimation(info.clip);
+				if (clip == null)
+					return false;
+
+				var time = AnimationTime(stateInfo.normalizedTime, info.clip.length,
+										info.clip.isLooping, stateInfo.speed < 0);
+				weight = useWeight1 ? 1.0f : weight;
+				clip.Apply(skeleton, 0, time, info.clip.isLooping, null,
+						weight, layerBlendMode, MixDirection.In);
+				if (_OnClipApplied != null)
+					OnClipAppliedCallback(clip, stateInfo, layerIndex, time, info.clip.isLooping, weight);
+				return true;
+			}
+
+			private bool ApplyInterruptionAnimation (Skeleton skeleton,
+				bool interpolateWeightTo1, AnimatorClipInfo info, AnimatorStateInfo stateInfo,
+				int layerIndex, float layerWeight, MixBlend layerBlendMode, float interruptingClipTimeAddition,
+				bool useWeight1 = false) {
+
+				float clipWeight = interpolateWeightTo1 ? (info.weight + 1.0f) * 0.5f : info.weight;
+				float weight = clipWeight * layerWeight;
+				if (weight == 0)
+					return false;
+
+				var clip = GetAnimation(info.clip);
+				if (clip == null)
+					return false;
+
+				var time = AnimationTime(stateInfo.normalizedTime + interruptingClipTimeAddition,
+										info.clip.length, stateInfo.speed < 0);
+				weight = useWeight1 ? 1.0f : weight;
+				clip.Apply(skeleton, 0, time, info.clip.isLooping, null,
+							weight, layerBlendMode, MixDirection.In);
+				if (_OnClipApplied != null) {
+					OnClipAppliedCallback(clip, stateInfo, layerIndex, time, info.clip.isLooping, weight);
+				}
+				return true;
+			}
+
+			private void OnClipAppliedCallback (Spine.Animation clip, AnimatorStateInfo stateInfo,
+				int layerIndex, float time, bool isLooping, float weight) {
+
+				float clipDuration = clip.duration == 0 ? 1 : clip.duration;
+				float speedFactor = stateInfo.speedMultiplier * stateInfo.speed;
+				float lastTime = time - (Time.deltaTime * speedFactor);
+				if (isLooping && clip.duration != 0) {
+					time %= clip.duration;
+					lastTime %= clip.duration;
+				}
+				_OnClipApplied(clip, layerIndex, weight, time, lastTime, speedFactor < 0);
+			}
+
 			public void Apply (Skeleton skeleton) {
 				if (layerMixModes.Length < animator.layerCount) {
+					int oldSize = layerMixModes.Length;
 					System.Array.Resize<MixMode>(ref layerMixModes, animator.layerCount);
-					layerMixModes[animator.layerCount-1] = MixMode.MixNext;
+					for (int layer = oldSize; layer < animator.layerCount; ++layer) {
+						layerMixModes[layer] = layer == 0 ? MixMode.MixNext : MixMode.AlwaysMix;
+					}
 				}
 
 			#if UNITY_EDITOR
@@ -257,56 +344,41 @@ namespace Spine.Unity {
 
 					int clipInfoCount, nextClipInfoCount, interruptingClipInfoCount;
 					IList<AnimatorClipInfo> clipInfo, nextClipInfo, interruptingClipInfo;
-					bool shallInterpolateWeightTo1;
+					bool interpolateWeightTo1;
 					GetAnimatorClipInfos(layer, out isInterruptionActive, out clipInfoCount, out nextClipInfoCount, out interruptingClipInfoCount,
-										out clipInfo, out nextClipInfo, out interruptingClipInfo, out shallInterpolateWeightTo1);
+										out clipInfo, out nextClipInfo, out interruptingClipInfo, out interpolateWeightTo1);
 
 					MixMode mode = layerMixModes[layer];
 					MixBlend layerBlendMode = (layer < layerBlendModes.Length) ? layerBlendModes[layer] : MixBlend.Replace;
 					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;
-							var clip = GetAnimation(info.clip);
-							if (clip != null)
-								clip.Apply(skeleton, 0, AnimationTime(stateInfo.normalizedTime, info.clip.length, info.clip.isLooping, stateInfo.speed < 0), info.clip.isLooping, null, weight, layerBlendMode, MixDirection.In);
+							ApplyAnimation(skeleton, clipInfo[c], stateInfo, layer, layerWeight, layerBlendMode);
 						}
 						if (hasNext) {
 							for (int c = 0; c < nextClipInfoCount; c++) {
-								var info = nextClipInfo[c]; float weight = info.weight * layerWeight; if (weight == 0) continue;
-								var clip = GetAnimation(info.clip);
-								if (clip != null)
-									clip.Apply(skeleton, 0, AnimationTime(nextStateInfo.normalizedTime, info.clip.length, nextStateInfo.speed < 0), info.clip.isLooping, null, weight, layerBlendMode, MixDirection.In);
+								ApplyAnimation(skeleton, nextClipInfo[c], nextStateInfo, layer, layerWeight, layerBlendMode);
 							}
 						}
 						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;
-								var clip = GetAnimation(info.clip);
-								if (clip != null)
-									clip.Apply(skeleton, 0, AnimationTime(interruptingStateInfo.normalizedTime + interruptingClipTimeAddition, info.clip.length, interruptingStateInfo.speed < 0),
-																info.clip.isLooping, null, weight, layerBlendMode, MixDirection.In);
+								ApplyInterruptionAnimation(skeleton, interpolateWeightTo1,
+									interruptingClipInfo[c], interruptingStateInfo,
+									layer, layerWeight, layerBlendMode, interruptingClipTimeAddition);
 							}
 						}
 					} else { // case MixNext || Hard
 						// 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;
-							var clip = GetAnimation(info.clip);
-							if (clip != null)
-								clip.Apply(skeleton, 0, AnimationTime(stateInfo.normalizedTime, info.clip.length, info.clip.isLooping, stateInfo.speed < 0), info.clip.isLooping, null, 1f, layerBlendMode, MixDirection.In);
+							if (!ApplyAnimation(skeleton, clipInfo[c], stateInfo, layer, layerWeight, layerBlendMode, useWeight1:true))
+								continue;
 							++c; break;
 						}
 						// Mix the rest
 						for (; c < clipInfoCount; c++) {
-							var info = clipInfo[c]; float weight = info.weight * layerWeight; if (weight == 0) continue;
-							var clip = GetAnimation(info.clip);
-							if (clip != null)
-								clip.Apply(skeleton, 0, AnimationTime(stateInfo.normalizedTime, info.clip.length, info.clip.isLooping, stateInfo.speed < 0), info.clip.isLooping, null, weight, layerBlendMode, MixDirection.In);
+							ApplyAnimation(skeleton, clipInfo[c], stateInfo, layer, layerWeight, layerBlendMode);
 						}
 
 						c = 0;
@@ -314,19 +386,15 @@ namespace Spine.Unity {
 							// Apply next clip directly instead of mixing (ie: no crossfade, ignores mecanim transition weights)
 							if (mode == MixMode.Hard) {
 								for (; c < nextClipInfoCount; c++) {
-									var info = nextClipInfo[c]; float weight = info.weight * layerWeight; if (weight == 0) continue;
-									var clip = GetAnimation(info.clip);
-									if (clip != null)
-										clip.Apply(skeleton, 0, AnimationTime(nextStateInfo.normalizedTime, info.clip.length, nextStateInfo.speed < 0), info.clip.isLooping, null, 1f, layerBlendMode, MixDirection.In);
+									if (!ApplyAnimation(skeleton, nextClipInfo[c], nextStateInfo, layer, layerWeight, layerBlendMode, useWeight1:true))
+										continue;
 									++c; break;
 								}
 							}
 							// Mix the rest
 							for (; c < nextClipInfoCount; c++) {
-								var info = nextClipInfo[c]; float weight = info.weight * layerWeight; if (weight == 0) continue;
-								var clip = GetAnimation(info.clip);
-								if (clip != null)
-									clip.Apply(skeleton, 0, AnimationTime(nextStateInfo.normalizedTime, info.clip.length, nextStateInfo.speed < 0), info.clip.isLooping, null, weight, layerBlendMode, MixDirection.In);
+								if (!ApplyAnimation(skeleton, nextClipInfo[c], nextStateInfo, layer, layerWeight, layerBlendMode))
+									continue;
 							}
 						}
 
@@ -335,23 +403,19 @@ namespace Spine.Unity {
 							// Apply next clip directly instead of mixing (ie: no crossfade, ignores mecanim transition weights)
 							if (mode == MixMode.Hard) {
 								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;
-									var clip = GetAnimation(info.clip);
-									if (clip != null)
-										clip.Apply(skeleton, 0, AnimationTime(interruptingStateInfo.normalizedTime + interruptingClipTimeAddition, info.clip.length, interruptingStateInfo.speed < 0), info.clip.isLooping, null, 1f, layerBlendMode, MixDirection.In);
-									++c; break;
+									if (ApplyInterruptionAnimation(skeleton, interpolateWeightTo1,
+										interruptingClipInfo[c], interruptingStateInfo,
+										layer, layerWeight, layerBlendMode, interruptingClipTimeAddition, useWeight1:true)) {
+
+										++c; 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;
-								var clip = GetAnimation(info.clip);
-								if (clip != null)
-									clip.Apply(skeleton, 0, AnimationTime(interruptingStateInfo.normalizedTime + interruptingClipTimeAddition, info.clip.length, interruptingStateInfo.speed < 0), info.clip.isLooping, null, weight, layerBlendMode, MixDirection.In);
+								ApplyInterruptionAnimation(skeleton, interpolateWeightTo1,
+									interruptingClipInfo[c], interruptingStateInfo,
+									layer, layerWeight, layerBlendMode, interruptingClipTimeAddition);
 							}
 						}
 					}
@@ -359,9 +423,7 @@ namespace Spine.Unity {
 			}
 
 			static float AnimationTime (float normalizedTime, float clipLength, bool loop, bool reversed) {
-				if (reversed)
-					normalizedTime = (1 - normalizedTime + (int)normalizedTime) + (int)normalizedTime;
-				float time = normalizedTime * clipLength;
+				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;