diff --git a/CHANGELOG.md b/CHANGELOG.md index eca45dc80..3ca2ad615 100644 --- a/CHANGELOG.md +++ b/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`. diff --git a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonAnimationInspector.cs b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonAnimationInspector.cs index 328766fda..c87ee6262 100644 --- a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonAnimationInspector.cs +++ b/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) { diff --git a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonGraphicInspector.cs b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonGraphicInspector.cs index 61044ed31..5f9c5942a 100644 --- a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonGraphicInspector.cs +++ b/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); diff --git a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonMecanimInspector.cs b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonMecanimInspector.cs index 4ecce5385..9f75dee10 100644 --- a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonMecanimInspector.cs +++ b/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(); + if (animator != null && animator.applyRootMotion) { + if (component.GetComponent() == null) { + component.gameObject.AddComponent(); + } + } + } + } + + 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; } } } diff --git a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonMecanimRootMotionInspector.cs b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonMecanimRootMotionInspector.cs new file mode 100644 index 000000000..bcdbbaab3 --- /dev/null +++ b/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); + } + } +} diff --git a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonMecanimRootMotionInspector.cs.meta b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonMecanimRootMotionInspector.cs.meta new file mode 100644 index 000000000..04c6dcfc9 --- /dev/null +++ b/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: diff --git a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonRendererInspector.cs b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonRendererInspector.cs index 4ada78b48..8942660dd 100644 --- a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonRendererInspector.cs +++ b/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() != 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() == null) { + component.gameObject.AddComponent(); + } + } + } + } + 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(); + if (rootMotionComponent != null) { + DestroyImmediate(rootMotionComponent); + } + } + } + } + } + } + public static void SetSeparatorSlotNames (SkeletonRenderer skeletonRenderer, string[] newSlotNames) { var field = SpineInspectorUtility.GetNonPublicField(typeof(SkeletonRenderer), SeparatorSlotNamesFieldName); field.SetValue(skeletonRenderer, newSlotNames); diff --git a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonRootMotionBaseInspector.cs b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonRootMotionBaseInspector.cs new file mode 100644 index 000000000..dd622e479 --- /dev/null +++ b/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); + } + } +} diff --git a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonRootMotionBaseInspector.cs.meta b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonRootMotionBaseInspector.cs.meta new file mode 100644 index 000000000..030444885 --- /dev/null +++ b/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: diff --git a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonRootMotionInspector.cs b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonRootMotionInspector.cs new file mode 100644 index 000000000..2c9650656 --- /dev/null +++ b/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); + } + } + } +} diff --git a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonRootMotionInspector.cs.meta b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonRootMotionInspector.cs.meta new file mode 100644 index 000000000..7c357a899 --- /dev/null +++ b/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: diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/RootMotion.meta b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/RootMotion.meta new file mode 100644 index 000000000..d8d3bd381 --- /dev/null +++ b/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: diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/RootMotion/SkeletonMecanimRootMotion.cs b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/RootMotion/SkeletonMecanimRootMotion.cs new file mode 100644 index 000000000..e49e8a486 --- /dev/null +++ b/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 { + + /// + /// 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. + /// + /// + /// Only compatible with SkeletonMecanim. + /// For SkeletonAnimation or SkeletonGraphic please use + /// SkeletonRootMotion instead. + /// + 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(); + } + } + + protected override void Reset () { + base.Reset(); + mecanimLayerFlags = DefaultMecanimLayerFlags; + } + + protected override void Start () { + base.Start(); + skeletonMecanim = GetComponent(); + 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< + /// 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. + /// + /// + /// Only compatible with SkeletonAnimation (or other components that implement + /// ISkeletonComponent, ISkeletonAnimation and IAnimationStateComponent). + /// For SkeletonMecanim please use + /// SkeletonMecanimRootMotion instead. + /// + 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() != null) { + canvas = this.GetComponentInParent(); + } + } + + 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; + } + } + } +} diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/RootMotion/SkeletonRootMotion.cs.meta b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/RootMotion/SkeletonRootMotion.cs.meta new file mode 100644 index 000000000..bef9e2e4f --- /dev/null +++ b/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: diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/RootMotion/SkeletonRootMotionBase.cs b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/RootMotion/SkeletonRootMotionBase.cs new file mode 100644 index 000000000..5b1e50dbb --- /dev/null +++ b/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 { + + /// + /// Base class for skeleton root motion components. + /// + 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 topLevelBones = new List(); + protected Vector2 rigidbodyDisplacement; + + protected virtual void Reset () { + FindRigidbodyComponent(); + } + + protected virtual void Start () { + skeletonComponent = GetComponent(); + 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(); + if (!rigidBody2D) + rigidBody = this.GetComponent(); + + if (!rigidBody2D && !rigidBody) { + rigidBody2D = this.GetComponentInParent(); + if (!rigidBody2D) + rigidBody = this.GetComponentInParent(); + } + } + } +} diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/RootMotion/SkeletonRootMotionBase.cs.meta b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/RootMotion/SkeletonRootMotionBase.cs.meta new file mode 100644 index 000000000..719fd629f --- /dev/null +++ b/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: diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonMecanim.cs b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonMecanim.cs index bfc993b35..83c6de8b0 100644 --- a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonMecanim.cs +++ b/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 animationTable = new Dictionary(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(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 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;