diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c60ef462..3a9b42903 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -399,6 +399,8 @@ * `BoneFollower` and `BoneFollowerGraphic` components now provide better support for following bones when the skeleton's Transform is not the parent of the follower's Transform. Previously e.g. rotating a common parent Transform did not lead to the desired result, as well as negatively scaling a skeleton's Transform when it is not a parent of the follower's Transform. * URP and LWRP `Sprite` and `SkeletonLit` shaders no longer require `Advanced - Add Normals` enabled to properly cast and receive shadows. It is recommended to disable `Add Normals` if normals are otherwise not needed. * Added an example component `RootMotionDeltaCompensation` located in `Spine Examples/Scripts/Sample Components` which can be used for applying simple delta compensation. You can enable and disable the component to toggle delta compensation of the currently playing animation on and off. + * Root motion delta compensation now allows to only adjust X or Y components instead of both. Adds two parameters to `SkeletonRootMotionBase.AdjustRootMotionToDistance()` which default to adjusting both X and Y as before. The `RootMotionDeltaCompensation` example component exposes these parameters as public attributes. + * Root motion delta compensation now allows to also add translation root motion to e.g. adjust a horizontal jump upwards or downwards over time. This is necessary because a Y root motion of zero cannot be scaled to become non-zero. * **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 Examples/Scripts/Sample Components/RootMotionDeltaCompensation.cs b/spine-unity/Assets/Spine Examples/Scripts/Sample Components/RootMotionDeltaCompensation.cs index 7731374ab..52f5f306e 100644 --- a/spine-unity/Assets/Spine Examples/Scripts/Sample Components/RootMotionDeltaCompensation.cs +++ b/spine-unity/Assets/Spine Examples/Scripts/Sample Components/RootMotionDeltaCompensation.cs @@ -5,12 +5,22 @@ namespace Spine.Unity.Examples { public class RootMotionDeltaCompensation : MonoBehaviour { - protected SkeletonRootMotionBase rootMotion; + [SerializeField] protected SkeletonRootMotionBase rootMotion; public Transform targetPosition; public int trackIndex = 0; + public bool adjustX = true; + public bool adjustY = true; + public float minScaleX = -999; + public float minScaleY = -999; + public float maxScaleX = 999; + public float maxScaleY = 999; + + public bool allowXTranslation = false; + public bool allowYTranslation = true; void Start () { - rootMotion = this.GetComponent(); + if (rootMotion == null) + rootMotion = this.GetComponent(); } void Update () { @@ -18,12 +28,21 @@ namespace Spine.Unity.Examples { } void OnDisable () { - rootMotion.rootMotionScaleX = rootMotion.rootMotionScaleY = 1; + if (adjustX) + rootMotion.rootMotionScaleX = 1; + if (adjustY) + rootMotion.rootMotionScaleY = 1; + if (allowXTranslation) + rootMotion.rootMotionTranslateXPerY = 0; + if (allowYTranslation) + rootMotion.rootMotionTranslateYPerX = 0; } void AdjustDelta() { Vector3 toTarget = targetPosition.position - this.transform.position; - rootMotion.AdjustRootMotionToDistance(toTarget, trackIndex); + rootMotion.AdjustRootMotionToDistance(toTarget, trackIndex, adjustX, adjustY, + minScaleX, maxScaleX, minScaleY, maxScaleY, + allowXTranslation, allowYTranslation); } } } 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 index 4f1af9540..8314eb092 100644 --- a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonRootMotionBaseInspector.cs +++ b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonRootMotionBaseInspector.cs @@ -39,6 +39,8 @@ namespace Spine.Unity.Editor { protected SerializedProperty transformPositionY; protected SerializedProperty rootMotionScaleX; protected SerializedProperty rootMotionScaleY; + protected SerializedProperty rootMotionTranslateXPerY; + protected SerializedProperty rootMotionTranslateYPerX; protected SerializedProperty rigidBody2D; protected SerializedProperty rigidBody; @@ -47,6 +49,8 @@ namespace Spine.Unity.Editor { protected GUIContent transformPositionYLabel; protected GUIContent rootMotionScaleXLabel; protected GUIContent rootMotionScaleYLabel; + protected GUIContent rootMotionTranslateXPerYLabel; + protected GUIContent rootMotionTranslateYPerXLabel; protected GUIContent rigidBody2DLabel; protected GUIContent rigidBodyLabel; @@ -57,6 +61,8 @@ namespace Spine.Unity.Editor { transformPositionY = serializedObject.FindProperty("transformPositionY"); rootMotionScaleX = serializedObject.FindProperty("rootMotionScaleX"); rootMotionScaleY = serializedObject.FindProperty("rootMotionScaleY"); + rootMotionTranslateXPerY = serializedObject.FindProperty("rootMotionTranslateXPerY"); + rootMotionTranslateYPerX = serializedObject.FindProperty("rootMotionTranslateYPerX"); rigidBody2D = serializedObject.FindProperty("rigidBody2D"); rigidBody = serializedObject.FindProperty("rigidBody"); @@ -65,6 +71,8 @@ namespace Spine.Unity.Editor { transformPositionYLabel = new UnityEngine.GUIContent("Y", "Use the Y-movement of the bone."); rootMotionScaleXLabel = new UnityEngine.GUIContent("Root Motion Scale (X)", "Scale applied to the horizontal root motion delta. Can be used for delta compensation to e.g. stretch a jump to the desired distance."); rootMotionScaleYLabel = new UnityEngine.GUIContent("Root Motion Scale (Y)", "Scale applied to the vertical root motion delta. Can be used for delta compensation to e.g. stretch a jump to the desired distance."); + rootMotionTranslateXPerYLabel = new UnityEngine.GUIContent("Root Motion Translate (X)", "Added X translation per root motion Y delta. Can be used for delta compensation when scaling is not enough, to e.g. offset a horizontal jump to a vertically different goal."); + rootMotionTranslateYPerXLabel = new UnityEngine.GUIContent("Root Motion Translate (Y)", "Added Y translation per root motion X delta. Can be used for delta compensation when scaling is not enough, to e.g. offset a horizontal jump to a vertically different goal."); 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." + @@ -92,10 +100,12 @@ namespace Spine.Unity.Editor { EditorGUILayout.PropertyField(rootMotionScaleX, rootMotionScaleXLabel); EditorGUILayout.PropertyField(rootMotionScaleY, rootMotionScaleYLabel); + + EditorGUILayout.PropertyField(rootMotionTranslateXPerY, rootMotionTranslateXPerYLabel); + EditorGUILayout.PropertyField(rootMotionTranslateYPerX, rootMotionTranslateYPerXLabel); } protected virtual void OptionalPropertyFields () { - //EditorGUILayout.LabelField("Optional", EditorStyles.boldLabel); EditorGUILayout.PropertyField(rigidBody2D, rigidBody2DLabel); EditorGUILayout.PropertyField(rigidBody, rigidBodyLabel); } 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 index e42e8ec27..4a90bdb00 100644 --- a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/RootMotion/SkeletonMecanimRootMotion.cs +++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/RootMotion/SkeletonMecanimRootMotion.cs @@ -73,6 +73,15 @@ namespace Spine.Unity { return GetAnimationRootMotion(start, end, animation); } + public override RootMotionInfo GetRootMotionInfo (int layerIndex) { + var pair = skeletonMecanim.Translator.GetActiveAnimationAndTime(layerIndex); + var animation = pair.Key; + var time = pair.Value; + if (animation == null) + return new RootMotionInfo(); + return GetAnimationRootMotionInfo(animation, time); + } + protected override void Reset () { base.Reset(); mecanimLayerFlags = DefaultMecanimLayerFlags; diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/RootMotion/SkeletonRootMotion.cs b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/RootMotion/SkeletonRootMotion.cs index b493af45f..b2293ba60 100644 --- a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/RootMotion/SkeletonRootMotion.cs +++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/RootMotion/SkeletonRootMotion.cs @@ -67,6 +67,16 @@ namespace Spine.Unity { return GetAnimationRootMotion(start, end, animation); } + public override RootMotionInfo GetRootMotionInfo (int trackIndex) { + TrackEntry track = animationState.GetCurrent(trackIndex); + if (track == null) + return new RootMotionInfo(); + + var animation = track.Animation; + float time = track.AnimationTime; + return GetAnimationRootMotionInfo(track.Animation, time); + } + protected override float AdditionalScale { get { return canvas ? canvas.referencePixelsPerUnit: 1.0f; 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 index 699cfb3da..a6222fb23 100644 --- a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/RootMotion/SkeletonRootMotionBase.cs +++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/RootMotion/SkeletonRootMotionBase.cs @@ -30,6 +30,7 @@ using UnityEngine; using System.Collections.Generic; using Spine.Unity.AnimationTools; +using System; namespace Spine.Unity { @@ -47,6 +48,10 @@ namespace Spine.Unity { public float rootMotionScaleX = 1; public float rootMotionScaleY = 1; + /// Skeleton space X translation per skeleton space Y translation root motion. + public float rootMotionTranslateXPerY = 0; + /// Skeleton space Y translation per skeleton space X translation root motion. + public float rootMotionTranslateYPerX = 0; [Header("Optional")] public Rigidbody2D rigidBody2D; @@ -62,6 +67,7 @@ namespace Spine.Unity { protected int rootMotionBoneIndex; protected List topLevelBones = new List(); protected Vector2 initialOffset = Vector2.zero; + protected Vector2 tempSkeletonDisplacement; protected Vector2 rigidbodyDisplacement; protected virtual void Reset () { @@ -94,11 +100,16 @@ namespace Spine.Unity { rigidBody.MovePosition(transform.position + new Vector3(rigidbodyDisplacement.x, rigidbodyDisplacement.y, 0)); } + Vector2 parentBoneScale; + GetScaleAffectingRootMotion(out parentBoneScale); + ClearEffectiveBoneOffsets(parentBoneScale); rigidbodyDisplacement = Vector2.zero; + tempSkeletonDisplacement = Vector2.zero; } protected virtual void OnDisable () { rigidbodyDisplacement = Vector2.zero; + tempSkeletonDisplacement = Vector2.zero; } protected void FindRigidbodyComponent () { @@ -117,6 +128,15 @@ namespace Spine.Unity { abstract protected Vector2 CalculateAnimationsMovementDelta (); abstract public Vector2 GetRemainingRootMotion (int trackIndex = 0); + public struct RootMotionInfo { + public Vector2 start; + public Vector2 current; + public Vector2 mid; + public Vector2 end; + public bool timeIsPastMid; + }; + abstract public RootMotionInfo GetRootMotionInfo (int trackIndex = 0); + public void SetRootMotionBone (string name) { var skeleton = skeletonComponent.Skeleton; int index = skeleton.FindBoneIndex(name); @@ -131,14 +151,31 @@ namespace Spine.Unity { } } - public void AdjustRootMotionToDistance (Vector2 distanceToTarget, int trackIndex = 0) { - Vector2 remainingRootMotion = GetRemainingRootMotion(trackIndex); - if (remainingRootMotion.x == 0) - remainingRootMotion.x = 0.0001f; - if (remainingRootMotion.y == 0) - remainingRootMotion.y = 0.0001f; - rootMotionScaleX = distanceToTarget.x / remainingRootMotion.x; - rootMotionScaleY = distanceToTarget.y / remainingRootMotion.y; + public void AdjustRootMotionToDistance (Vector2 distanceToTarget, int trackIndex = 0, bool adjustX = true, bool adjustY = true, + float minX = 0, float maxX = float.MaxValue, float minY = 0, float maxY = float.MaxValue, + bool allowXTranslation = false, bool allowYTranslation = false) { + + Vector2 distanceToTargetSkeletonSpace = (Vector2)transform.InverseTransformVector(distanceToTarget); + Vector2 scaleAffectingRootMotion = GetScaleAffectingRootMotion(); + if (UsesRigidbody) + distanceToTargetSkeletonSpace -= tempSkeletonDisplacement; + + Vector2 remainingRootMotionSkeletonSpace = GetRemainingRootMotion(trackIndex); + remainingRootMotionSkeletonSpace.Scale(scaleAffectingRootMotion); + if (remainingRootMotionSkeletonSpace.x == 0) + remainingRootMotionSkeletonSpace.x = 0.0001f; + if (remainingRootMotionSkeletonSpace.y == 0) + remainingRootMotionSkeletonSpace.y = 0.0001f; + + if (adjustX) + rootMotionScaleX = Math.Min(maxX, Math.Max(minX, distanceToTargetSkeletonSpace.x / remainingRootMotionSkeletonSpace.x)); + if (adjustY) + rootMotionScaleY = Math.Min(maxY, Math.Max(minY, distanceToTargetSkeletonSpace.y / remainingRootMotionSkeletonSpace.y)); + + if (allowXTranslation) + rootMotionTranslateXPerY = (distanceToTargetSkeletonSpace.x - remainingRootMotionSkeletonSpace.x * rootMotionScaleX) / remainingRootMotionSkeletonSpace.y; + if (allowYTranslation) + rootMotionTranslateYPerX = (distanceToTargetSkeletonSpace.y - remainingRootMotionSkeletonSpace.y * rootMotionScaleY) / remainingRootMotionSkeletonSpace.x; } public Vector2 GetAnimationRootMotion (Animation animation) { @@ -155,6 +192,21 @@ namespace Spine.Unity { return Vector2.zero; } + public RootMotionInfo GetAnimationRootMotionInfo (Animation animation, float currentTime) { + RootMotionInfo rootMotion = new RootMotionInfo(); + var timeline = animation.FindTranslateTimelineForBone(rootMotionBoneIndex); + if (timeline != null) { + float duration = animation.duration; + float mid = duration * 0.5f; + rootMotion.start = timeline.Evaluate(0); + rootMotion.current = timeline.Evaluate(currentTime); + rootMotion.mid = timeline.Evaluate(mid); + rootMotion.end = timeline.Evaluate(duration); + rootMotion.timeIsPastMid = currentTime > mid; + } + return rootMotion; + } + Vector2 GetTimelineMovementDelta (float startTime, float endTime, TranslateTimeline timeline, Animation animation) { @@ -182,15 +234,39 @@ namespace Spine.Unity { if (!this.isActiveAndEnabled) return; // Root motion is only applied when component is enabled. - var movementDelta = CalculateAnimationsMovementDelta(); + var boneLocalDelta = CalculateAnimationsMovementDelta(); Vector2 parentBoneScale; - AdjustMovementDeltaToConfiguration(ref movementDelta, out parentBoneScale, animatedSkeletonComponent.Skeleton); - ApplyRootMotion(movementDelta, parentBoneScale); + Vector2 skeletonDelta = GetSkeletonSpaceMovementDelta(boneLocalDelta, out parentBoneScale); + ApplyRootMotion(skeletonDelta, parentBoneScale); } - void AdjustMovementDeltaToConfiguration (ref Vector2 localDelta, out Vector2 parentBoneScale, Skeleton skeleton) { - localDelta.x *= skeleton.ScaleX; - localDelta.y *= skeleton.ScaleY; + void ApplyRootMotion (Vector2 skeletonDelta, Vector2 parentBoneScale) { + // Apply root motion to Transform or RigidBody; + if (UsesRigidbody) { + rigidbodyDisplacement += (Vector2)transform.TransformVector(skeletonDelta); + + // Accumulated displacement is applied on the next Physics update in FixedUpdate. + // Until the next Physics update, tempBoneDisplacement is offsetting bone locations + // to prevent stutter which would otherwise occur if we don't move every Update. + tempSkeletonDisplacement += skeletonDelta; + SetEffectiveBoneOffsetsTo(tempSkeletonDisplacement, parentBoneScale); + } + else { + transform.position += transform.TransformVector(skeletonDelta); + ClearEffectiveBoneOffsets(parentBoneScale); + } + } + + Vector2 GetScaleAffectingRootMotion () { + Vector2 parentBoneScale; + return GetScaleAffectingRootMotion(out parentBoneScale); + } + + Vector2 GetScaleAffectingRootMotion (out Vector2 parentBoneScale) { + var skeleton = skeletonComponent.Skeleton; + Vector2 totalScale = Vector2.one; + totalScale.x *= skeleton.ScaleX; + totalScale.y *= skeleton.ScaleY; parentBoneScale = Vector2.one; Bone scaleBone = rootMotionBone; @@ -198,37 +274,49 @@ namespace Spine.Unity { parentBoneScale.x *= scaleBone.ScaleX; parentBoneScale.y *= scaleBone.ScaleY; } - localDelta = Vector2.Scale(localDelta, parentBoneScale); - - localDelta *= AdditionalScale; - localDelta.x *= rootMotionScaleX; - localDelta.y *= rootMotionScaleY; - - if (!transformPositionX) localDelta.x = 0f; - if (!transformPositionY) localDelta.y = 0f; + totalScale = Vector2.Scale(totalScale, parentBoneScale); + totalScale *= AdditionalScale; + return totalScale; } - void ApplyRootMotion (Vector2 localDelta, Vector2 parentBoneScale) { - // 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); - } + Vector2 GetSkeletonSpaceMovementDelta (Vector2 boneLocalDelta, out Vector2 parentBoneScale) { + Vector2 skeletonDelta = boneLocalDelta; + Vector2 totalScale = GetScaleAffectingRootMotion(out parentBoneScale); + skeletonDelta.Scale(totalScale); + Vector2 rootMotionTranslation = new Vector2( + rootMotionTranslateXPerY * skeletonDelta.y, + rootMotionTranslateYPerX * skeletonDelta.x); + + skeletonDelta.x *= rootMotionScaleX; + skeletonDelta.y *= rootMotionScaleY; + skeletonDelta.x += rootMotionTranslation.x; + skeletonDelta.y += rootMotionTranslation.y; + + if (!transformPositionX) skeletonDelta.x = 0f; + if (!transformPositionY) skeletonDelta.y = 0f; + return skeletonDelta; + } + + void SetEffectiveBoneOffsetsTo (Vector2 displacementSkeletonSpace, Vector2 parentBoneScale) { // Move top level bones in opposite direction of the root motion bone + var skeleton = skeletonComponent.Skeleton; foreach (var topLevelBone in topLevelBones) { if (topLevelBone == rootMotionBone) { - if (transformPositionX) topLevelBone.x = 0; - if (transformPositionY) topLevelBone.y = 0; + if (transformPositionX) topLevelBone.x = displacementSkeletonSpace.x / skeleton.ScaleX; + if (transformPositionY) topLevelBone.y = displacementSkeletonSpace.y / skeleton.ScaleY; } else { - if (transformPositionX) topLevelBone.x = -(rootMotionBone.x - initialOffset.x) * parentBoneScale.x; - if (transformPositionY) topLevelBone.y = -(rootMotionBone.y - initialOffset.y) * parentBoneScale.y; + float offsetX = (initialOffset.x - rootMotionBone.x) * parentBoneScale.x; + float offsetY = (initialOffset.y - rootMotionBone.y) * parentBoneScale.y; + if (transformPositionX) topLevelBone.x = (displacementSkeletonSpace.x / skeleton.ScaleX) + offsetX; + if (transformPositionY) topLevelBone.y = (displacementSkeletonSpace.y / skeleton.ScaleY) + offsetY; } } } + + void ClearEffectiveBoneOffsets (Vector2 parentBoneScale) { + SetEffectiveBoneOffsetsTo(Vector2.zero, parentBoneScale); + } } }