diff --git a/CHANGELOG.md b/CHANGELOG.md
index 223e4d9bf..711858392 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -86,6 +86,7 @@
* Added `UnscaledTime` property at `SkeletonAnimation` as well, behaving like `SkeletonGraphic.UnscaledTime`. If enabled, AnimationState uses unscaled game time (`Time.unscaledDeltaTime`), running animations independent of e.g. game pause (`Time.timeScale`).
* `SkeletonAnimation`, `SkeletonMecanim` and `SkeletonGraphic` now provide an additional `OnAnimationRebuild` callback delegate which is issued after both the skeleton and the animation state have been initialized.
* Timeline `SkeletonAnimation Track` and `SkeletonGraphic Track` now provide an `Unscaled Time` property. Whenever starting a new animation clip of this track, `SkeletonAnimation.UnscaledTime` or `SkeletonGraphic.UnscaledTime` will be set to this value. This allows you to play back Timeline clips either in normal game time or unscaled game time. Note that `PlayableDirector.UpdateMethod` is ignored and replaced by this property, which allows more fine-granular control per Timeline track.
+ * Added `SkeletonRootMotion` callback delegates `ProcessRootMotionOverride` and `PhysicsUpdateRootMotionOverride` to customize how root motion is applied. The new property `disableOnOverride` determines whether the callback will be issued in addition or instead of normally applying root motion. Added property `rootMotionScaleRotation` to allow scaling rotational root-motion to match e.g. a 90 degree rotation to a custom target angle.
* **Breaking changes**
* Made `SkeletonGraphic.unscaledTime` parameter protected, use the new property `UnscaledTime` instead.
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 0ea72931e..809f2157e 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
@@ -40,6 +40,7 @@ namespace Spine.Unity.Editor {
protected SerializedProperty transformRotation;
protected SerializedProperty rootMotionScaleX;
protected SerializedProperty rootMotionScaleY;
+ protected SerializedProperty rootMotionScaleRotation;
protected SerializedProperty rootMotionTranslateXPerY;
protected SerializedProperty rootMotionTranslateYPerX;
protected SerializedProperty rigidBody2D;
@@ -52,6 +53,7 @@ namespace Spine.Unity.Editor {
protected GUIContent transformRotationLabel;
protected GUIContent rootMotionScaleXLabel;
protected GUIContent rootMotionScaleYLabel;
+ protected GUIContent rootMotionScaleRotationLabel;
protected GUIContent rootMotionTranslateXPerYLabel;
protected GUIContent rootMotionTranslateYPerXLabel;
protected GUIContent rigidBody2DLabel;
@@ -66,6 +68,7 @@ namespace Spine.Unity.Editor {
transformRotation = serializedObject.FindProperty("transformRotation");
rootMotionScaleX = serializedObject.FindProperty("rootMotionScaleX");
rootMotionScaleY = serializedObject.FindProperty("rootMotionScaleY");
+ rootMotionScaleRotation = serializedObject.FindProperty("rootMotionScaleRotation");
rootMotionTranslateXPerY = serializedObject.FindProperty("rootMotionTranslateXPerY");
rootMotionTranslateYPerX = serializedObject.FindProperty("rootMotionTranslateYPerX");
rigidBody2D = serializedObject.FindProperty("rigidBody2D");
@@ -78,6 +81,7 @@ namespace Spine.Unity.Editor {
transformRotationLabel = new UnityEngine.GUIContent("Rotation", "Use the rotation 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.");
+ rootMotionScaleRotationLabel = new UnityEngine.GUIContent("Root Motion Scale (Rotation)", "Scale applied to the rotational root motion delta. Can be used for delta compensation to e.g. adjust an angled jump landing to the desired platform angle.");
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",
@@ -110,6 +114,7 @@ namespace Spine.Unity.Editor {
EditorGUILayout.PropertyField(rootMotionScaleX, rootMotionScaleXLabel);
EditorGUILayout.PropertyField(rootMotionScaleY, rootMotionScaleYLabel);
+ EditorGUILayout.PropertyField(rootMotionScaleRotation, rootMotionScaleRotationLabel);
EditorGUILayout.PropertyField(rootMotionTranslateXPerY, rootMotionTranslateXPerYLabel);
EditorGUILayout.PropertyField(rootMotionTranslateYPerX, rootMotionTranslateYPerXLabel);
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 4898f0f2b..3cf3f4e9c 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
@@ -50,6 +50,7 @@ namespace Spine.Unity {
public float rootMotionScaleX = 1;
public float rootMotionScaleY = 1;
+ public float rootMotionScaleRotation = 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.
@@ -60,6 +61,36 @@ namespace Spine.Unity {
public bool applyRigidbody2DGravity = false;
public Rigidbody rigidBody;
+ /// Delegate type for customizing application of rootmotion.
+ public delegate void RootMotionDelegate (SkeletonRootMotionBase component, Vector2 translation, float rotation);
+ /// This callback can be used to apply root-motion in a custom way. It is raised after evaluating
+ /// this animation frame's root-motion, before it is potentially applied (see )
+ /// to either Transform or Rigidbody.
+ /// When is set to , multiple
+ /// animation frames might take place before FixedUpdate is called once.
+ /// The callback parameters translation and rotation are filled out with
+ /// this animation frame's skeleton-space root-motion (not cumulated). You can use
+ /// e.g. transform.TransformVector() to transform skeleton-space root-motion to world space.
+ ///
+ ///
+ public event RootMotionDelegate ProcessRootMotionOverride;
+ /// This callback can be used to apply root-motion in a custom way. It is raised in FixedUpdate
+ /// after (when is set to false) or instead of when root-motion
+ /// would be applied at the Rigidbody.
+ /// When is set to , multiple
+ /// animation frames might take place before before FixedUpdate is called once.
+ /// The callback parameters translation and rotation are filled out with the
+ /// (cumulated) skeleton-space root-motion since the the last FixedUpdate call. You can use
+ /// e.g. transform.TransformVector() to transform skeleton-space root-motion to world space.
+ ///
+ ///
+ public event RootMotionDelegate PhysicsUpdateRootMotionOverride;
+ /// When true, root-motion is not applied to the Transform or Rigidbody.
+ /// Otherwise the delegate callbacks are issued additionally.
+ public bool disableOnOverride = true;
+
+ public Bone RootMotionBone { get { return rootMotionBone; } }
+
public bool UsesRigidbody {
get { return rigidBody != null || rigidBody2D != null; }
}
@@ -108,12 +139,13 @@ namespace Spine.Unity {
protected List transformConstraintLastRotation = new List();
protected List topLevelBones = new List();
protected Vector2 initialOffset = Vector2.zero;
+ protected bool accumulatedUntilFixedUpdate = false;
protected Vector2 tempSkeletonDisplacement;
protected Vector3 rigidbodyDisplacement;
protected Vector3 previousRigidbodyRootMotion = Vector2.zero;
protected Vector2 additionalRigidbody2DMovement = Vector2.zero;
- protected Quaternion rigidbodyRotation = Quaternion.identity;
+ protected Quaternion rigidbodyLocalRotation = Quaternion.identity;
protected float rigidbody2DRotation;
protected float initialOffsetRotation;
protected float tempSkeletonRotation;
@@ -149,35 +181,44 @@ namespace Spine.Unity {
}
protected virtual void PhysicsUpdate (bool skeletonAnimationUsesFixedUpdate) {
- if (rigidBody2D != null) {
- Vector2 gravityAndVelocityMovement = Vector2.zero;
- if (applyRigidbody2DGravity) {
- float deltaTime = Time.fixedDeltaTime;
- float deltaTimeSquared = (deltaTime * deltaTime);
+ Vector2 callbackDisplacement = tempSkeletonDisplacement;
+ float callbackRotation = tempSkeletonRotation;
- rigidBody2D.velocity += rigidBody2D.gravityScale * Physics2D.gravity * deltaTime;
- gravityAndVelocityMovement = 0.5f * rigidBody2D.gravityScale * Physics2D.gravity * deltaTimeSquared +
- rigidBody2D.velocity * deltaTime;
+ bool isApplyAtRigidbodyAllowed = PhysicsUpdateRootMotionOverride == null || !disableOnOverride;
+ if (isApplyAtRigidbodyAllowed) {
+ if (rigidBody2D != null) {
+ Vector2 gravityAndVelocityMovement = Vector2.zero;
+ if (applyRigidbody2DGravity) {
+ float deltaTime = Time.fixedDeltaTime;
+ float deltaTimeSquared = (deltaTime * deltaTime);
+
+ rigidBody2D.velocity += rigidBody2D.gravityScale * Physics2D.gravity * deltaTime;
+ gravityAndVelocityMovement = 0.5f * rigidBody2D.gravityScale * Physics2D.gravity * deltaTimeSquared +
+ rigidBody2D.velocity * deltaTime;
+ }
+
+ Vector2 rigidbodyDisplacement2D = new Vector2(rigidbodyDisplacement.x, rigidbodyDisplacement.y);
+ rigidBody2D.MovePosition(gravityAndVelocityMovement + new Vector2(transform.position.x, transform.position.y)
+ + rigidbodyDisplacement2D + additionalRigidbody2DMovement);
+ rigidBody2D.MoveRotation(rigidbody2DRotation + rigidBody2D.rotation);
+ } else if (rigidBody != null) {
+ rigidBody.MovePosition(transform.position
+ + new Vector3(rigidbodyDisplacement.x, rigidbodyDisplacement.y, rigidbodyDisplacement.z));
+ rigidBody.MoveRotation(rigidBody.rotation * rigidbodyLocalRotation);
}
+ }
- Vector2 rigidbodyDisplacement2D = new Vector2(rigidbodyDisplacement.x, rigidbodyDisplacement.y);
- rigidBody2D.MovePosition(gravityAndVelocityMovement + new Vector2(transform.position.x, transform.position.y)
- + rigidbodyDisplacement2D + additionalRigidbody2DMovement);
- rigidBody2D.MoveRotation(rigidbody2DRotation + rigidBody2D.rotation);
- } else if (rigidBody != null) {
- rigidBody.MovePosition(transform.position
- + new Vector3(rigidbodyDisplacement.x, rigidbodyDisplacement.y, 0));
- rigidBody.MoveRotation(rigidBody.rotation * rigidbodyRotation);
- } else return;
-
- Vector2 parentBoneScale;
- GetScaleAffectingRootMotion(out parentBoneScale);
- if (!skeletonAnimationUsesFixedUpdate) {
+ previousRigidbodyRootMotion = rigidbodyDisplacement;
+ if (accumulatedUntilFixedUpdate) {
+ Vector2 parentBoneScale;
+ GetScaleAffectingRootMotion(out parentBoneScale);
ClearEffectiveBoneOffsets(parentBoneScale);
skeletonComponent.Skeleton.UpdateWorldTransform();
}
- previousRigidbodyRootMotion = rigidbodyDisplacement;
ClearRigidbodyTempMovement();
+
+ if (PhysicsUpdateRootMotionOverride != null)
+ PhysicsUpdateRootMotionOverride(this, callbackDisplacement, callbackRotation);
}
protected virtual void OnDisable () {
@@ -499,6 +540,7 @@ namespace Spine.Unity {
float skeletonRotationDelta = 0;
if (transformRotation) {
float boneLocalDeltaRotation = CalculateAnimationsRotationDelta();
+ boneLocalDeltaRotation *= rootMotionScaleRotation;
skeletonRotationDelta = GetSkeletonSpaceRotationDelta(boneLocalDeltaRotation, totalScale);
}
@@ -511,40 +553,45 @@ namespace Spine.Unity {
void ApplyRootMotion (Vector2 skeletonTranslationDelta, float skeletonRotationDelta, Vector2 parentBoneScale,
bool skeletonAnimationUsesFixedUpdate) {
- // Apply root motion to Transform or RigidBody;
- if (UsesRigidbody) {
+
+ // Accumulated displacement is applied on the next Physics update in FixedUpdate.
+ // Until the next Physics update, tempSkeletonDisplacement and tempSkeletonRotation
+ // are offsetting bone locations to prevent stutter which would otherwise occur if
+ // we don't move every Update.
+ bool usesRigidbody = this.UsesRigidbody;
+ bool applyToTransform = !usesRigidbody && (ProcessRootMotionOverride == null || !disableOnOverride);
+ accumulatedUntilFixedUpdate = !applyToTransform && !skeletonAnimationUsesFixedUpdate;
+
+ if (ProcessRootMotionOverride != null)
+ ProcessRootMotionOverride(this, skeletonTranslationDelta, skeletonRotationDelta);
+
+ // Apply root motion to Transform or update values applied to RigidBody later (must happen in FixedUpdate).
+ if (usesRigidbody) {
rigidbodyDisplacement += transform.TransformVector(skeletonTranslationDelta);
-
- // 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.
- if (!skeletonAnimationUsesFixedUpdate)
- tempSkeletonDisplacement += skeletonTranslationDelta;
-
if (skeletonRotationDelta != 0.0f) {
if (rigidBody != null) {
- Quaternion addedWorldRotation = Quaternion.AngleAxis(skeletonRotationDelta, transform.forward);
- rigidbodyRotation = rigidbodyRotation * addedWorldRotation;
+ Quaternion addedWorldRotation = Quaternion.Euler(0, 0, skeletonRotationDelta);
+ rigidbodyLocalRotation = rigidbodyLocalRotation * addedWorldRotation;
} else if (rigidBody2D != null) {
Vector3 lossyScale = transform.lossyScale;
float rotationSign = lossyScale.x * lossyScale.y > 0 ? 1 : -1;
rigidbody2DRotation += rotationSign * skeletonRotationDelta;
}
- if (!skeletonAnimationUsesFixedUpdate)
- tempSkeletonRotation += skeletonRotationDelta;
}
-
- if (skeletonAnimationUsesFixedUpdate)
- ClearEffectiveBoneOffsets(parentBoneScale);
- else
- SetEffectiveBoneOffsetsTo(tempSkeletonDisplacement, tempSkeletonRotation, parentBoneScale);
- } else {
+ } else if (applyToTransform) {
transform.position += transform.TransformVector(skeletonTranslationDelta);
if (skeletonRotationDelta != 0.0f) {
Vector3 lossyScale = transform.lossyScale;
float rotationSign = lossyScale.x * lossyScale.y > 0 ? 1 : -1;
transform.Rotate(0, 0, rotationSign * skeletonRotationDelta);
}
+ }
+
+ tempSkeletonDisplacement += skeletonTranslationDelta;
+ tempSkeletonRotation += skeletonRotationDelta;
+ if (accumulatedUntilFixedUpdate) {
+ SetEffectiveBoneOffsetsTo(tempSkeletonDisplacement, tempSkeletonRotation, parentBoneScale);
+ } else {
ClearEffectiveBoneOffsets(parentBoneScale);
}
}
@@ -654,7 +701,7 @@ namespace Spine.Unity {
void ClearRigidbodyTempMovement () {
rigidbodyDisplacement = Vector2.zero;
tempSkeletonDisplacement = Vector2.zero;
- rigidbodyRotation = Quaternion.identity;
+ rigidbodyLocalRotation = Quaternion.identity;
rigidbody2DRotation = 0;
tempSkeletonRotation = 0;
}