/******************************************************************************
* Spine Runtimes License Agreement
* Last updated July 28, 2023. Replaces all prior versions.
*
* Copyright (c) 2013-2023, 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.
*****************************************************************************/
// In order to respect TransformConstraints modifying the scale of parent bones,
// GetScaleAffectingRootMotion() now uses parentBone.AScaleX and AScaleY instead
// of previously used ScaleX and ScaleY. If you require the previous behaviour,
// comment out the define below.
#define USE_APPLIED_PARENT_SCALE
using Spine.Unity.AnimationTools;
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Spine.Unity {
///
/// Base class for skeleton root motion components.
///
[DefaultExecutionOrder(1)]
abstract public class SkeletonRootMotionBase : MonoBehaviour {
#region Inspector
[SpineBone]
public string rootMotionBoneName = "root";
public bool transformPositionX = true;
public bool transformPositionY = true;
public bool transformRotation = false;
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.
public float rootMotionTranslateYPerX = 0;
[Header("Optional")]
public Rigidbody2D rigidBody2D;
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; }
}
/// Root motion translation that has been applied in the preceding FixedUpdate call
/// if a rigidbody is assigned at either rigidbody or rigidbody2D.
/// Returns Vector2.zero when rigidbody and rigidbody2D are null.
/// This can be necessary when multiple scripts call Rigidbody2D.MovePosition,
/// where the last call overwrites the effect of preceding ones.
public Vector2 PreviousRigidbodyRootMotion2D {
get { return new Vector2(previousRigidbodyRootMotion.x, previousRigidbodyRootMotion.y); }
}
/// Root motion translation that has been applied in the preceding FixedUpdate call
/// if a rigidbody is assigned at either rigidbody or rigidbody2D.
/// Returns Vector3.zero when rigidbody and rigidbody2D are null.
public Vector3 PreviousRigidbodyRootMotion3D {
get { return previousRigidbodyRootMotion; }
}
/// Additional translation to add to Rigidbody2D.MovePosition
/// called in FixedUpdate. This can be necessary when multiple scripts call
/// MovePosition, where the last call overwrites the effect of preceding ones.
/// Has no effect if rigidBody2D is null.
public Vector2 AdditionalRigidbody2DMovement {
get { return additionalRigidbody2DMovement; }
set { additionalRigidbody2DMovement = value; }
}
#endregion
protected bool SkeletonAnimationUsesFixedUpdate {
get {
ISkeletonAnimation skeletonAnimation = skeletonComponent as ISkeletonAnimation;
if (skeletonAnimation != null) {
return skeletonAnimation.UpdateTiming == UpdateTiming.InFixedUpdate;
}
return false;
}
}
protected ISkeletonComponent skeletonComponent;
protected Bone rootMotionBone;
protected int rootMotionBoneIndex;
protected List transformConstraintIndices = new List();
protected List transformConstraintLastPos = new List();
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 rigidbodyLocalRotation = Quaternion.identity;
protected float rigidbody2DRotation;
protected float initialOffsetRotation;
protected float tempSkeletonRotation;
protected virtual void Reset () {
FindRigidbodyComponent();
}
protected virtual void Start () {
Initialize();
}
protected void InitializeOnRebuild (ISkeletonAnimation animatedSkeletonComponent) {
Initialize();
}
public virtual void Initialize () {
skeletonComponent = GetComponent();
GatherTopLevelBones();
SetRootMotionBone(rootMotionBoneName);
if (rootMotionBone != null) {
initialOffset = new Vector2(rootMotionBone.X, rootMotionBone.Y);
initialOffsetRotation = rootMotionBone.Rotation;
}
ISkeletonAnimation skeletonAnimation = skeletonComponent as ISkeletonAnimation;
if (skeletonAnimation != null) {
skeletonAnimation.UpdateLocal -= HandleUpdateLocal;
skeletonAnimation.UpdateLocal += HandleUpdateLocal;
skeletonAnimation.OnAnimationRebuild -= InitializeOnRebuild;
skeletonAnimation.OnAnimationRebuild += InitializeOnRebuild;
SkeletonUtility skeletonUtility = GetComponent();
if (skeletonUtility != null) {
// SkeletonUtilityBone shall receive UpdateLocal callbacks for bone-following after root motion
// clears the root-bone position.
skeletonUtility.ResubscribeEvents();
}
}
}
protected virtual void FixedUpdate () {
// Root motion is only applied when component is enabled.
if (!this.isActiveAndEnabled)
return;
// When SkeletonAnimation component uses UpdateTiming.InFixedUpdate,
// we directly call PhysicsUpdate in HandleUpdateLocal instead of here.
if (!SkeletonAnimationUsesFixedUpdate)
PhysicsUpdate(false);
}
protected virtual void PhysicsUpdate (bool skeletonAnimationUsesFixedUpdate) {
Vector2 callbackDisplacement = tempSkeletonDisplacement;
float callbackRotation = tempSkeletonRotation;
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);
// Note: MovePosition seems to be the only precise and reliable way to set movement delta,
// for both 2D and 3D rigidbodies.
// Setting velocity like "rigidBody2D.velocity = movement/deltaTime" works perfectly in mid-air
// without gravity and ground collision, unfortunately when on the ground, friction causes severe
// slowdown. Using a zero-friction PhysicsMaterial leads to sliding endlessly along the ground as
// soon as forces are applied. Additionally, there is no rigidBody2D.isGrounded, requiring our own
// checks.
rigidBody2D.MovePosition(gravityAndVelocityMovement + new Vector2(rigidBody2D.position.x, rigidBody2D.position.y)
+ rigidbodyDisplacement2D + additionalRigidbody2DMovement);
rigidBody2D.MoveRotation(rigidbody2DRotation + rigidBody2D.rotation);
} else if (rigidBody != null) {
rigidBody.MovePosition(rigidBody.position
+ new Vector3(rigidbodyDisplacement.x, rigidbodyDisplacement.y, rigidbodyDisplacement.z));
rigidBody.MoveRotation(rigidBody.rotation * rigidbodyLocalRotation);
}
}
previousRigidbodyRootMotion = rigidbodyDisplacement;
if (accumulatedUntilFixedUpdate) {
Vector2 parentBoneScale;
GetScaleAffectingRootMotion(out parentBoneScale);
ClearEffectiveBoneOffsets(parentBoneScale);
skeletonComponent.Skeleton.UpdateWorldTransform(Skeleton.Physics.Pose);
}
ClearRigidbodyTempMovement();
if (PhysicsUpdateRootMotionOverride != null)
PhysicsUpdateRootMotionOverride(this, callbackDisplacement, callbackRotation);
}
protected virtual void OnDisable () {
ClearRigidbodyTempMovement();
}
protected void FindRigidbodyComponent () {
rigidBody2D = this.GetComponent();
if (!rigidBody2D)
rigidBody = this.GetComponent();
if (!rigidBody2D && !rigidBody) {
rigidBody2D = this.GetComponentInParent();
if (!rigidBody2D)
rigidBody = this.GetComponentInParent();
}
}
protected virtual float AdditionalScale { get { return 1.0f; } }
abstract protected Vector2 CalculateAnimationsMovementDelta ();
protected virtual float CalculateAnimationsRotationDelta () { return 0; }
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 ISkeletonComponent TargetSkeletonComponent {
get {
if (skeletonComponent == null)
skeletonComponent = GetComponent();
return skeletonComponent;
}
}
public ISkeletonAnimation TargetSkeletonAnimationComponent {
get { return TargetSkeletonComponent as ISkeletonAnimation; }
}
public void SetRootMotionBone (string name) {
Skeleton skeleton = skeletonComponent.Skeleton;
Bone bone = skeleton.FindBone(name);
if (bone != null) {
this.rootMotionBoneIndex = bone.Data.Index;
this.rootMotionBone = bone;
FindTransformConstraintsAffectingBone();
} else {
Debug.Log("Bone named \"" + name + "\" could not be found. " +
"Set 'skeletonRootMotion.rootMotionBoneName' before calling 'skeletonAnimation.Initialize(true)'.");
this.rootMotionBoneIndex = 0;
this.rootMotionBone = skeleton.RootBone;
}
}
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) {
return GetAnimationRootMotion(0, animation.Duration, animation);
}
public Vector2 GetAnimationRootMotion (float startTime, float endTime,
Animation animation) {
if (startTime == endTime)
return Vector2.zero;
TranslateTimeline translateTimeline = animation.FindTranslateTimelineForBone(rootMotionBoneIndex);
TranslateXTimeline xTimeline = animation.FindTimelineForBone(rootMotionBoneIndex);
TranslateYTimeline yTimeline = animation.FindTimelineForBone(rootMotionBoneIndex);
// Non-looped base
Vector2 endPos = Vector2.zero;
Vector2 startPos = Vector2.zero;
if (translateTimeline != null) {
endPos = translateTimeline.Evaluate(endTime);
startPos = translateTimeline.Evaluate(startTime);
} else if (xTimeline != null || yTimeline != null) {
endPos = TimelineExtensions.Evaluate(xTimeline, yTimeline, endTime);
startPos = TimelineExtensions.Evaluate(xTimeline, yTimeline, startTime);
}
TransformConstraint[] transformConstraintsItems = skeletonComponent.Skeleton.TransformConstraints.Items;
foreach (int constraintIndex in this.transformConstraintIndices) {
TransformConstraint constraint = transformConstraintsItems[constraintIndex];
ApplyConstraintToPos(animation, constraint, constraintIndex, endTime, false, ref endPos);
ApplyConstraintToPos(animation, constraint, constraintIndex, startTime, true, ref startPos);
}
Vector2 currentDelta = endPos - startPos;
// Looped additions
if (startTime > endTime) {
Vector2 loopPos = Vector2.zero;
Vector2 zeroPos = Vector2.zero;
if (translateTimeline != null) {
loopPos = translateTimeline.Evaluate(animation.Duration);
zeroPos = translateTimeline.Evaluate(0);
} else if (xTimeline != null || yTimeline != null) {
loopPos = TimelineExtensions.Evaluate(xTimeline, yTimeline, animation.Duration);
zeroPos = TimelineExtensions.Evaluate(xTimeline, yTimeline, 0);
}
foreach (int constraintIndex in this.transformConstraintIndices) {
TransformConstraint constraint = transformConstraintsItems[constraintIndex];
ApplyConstraintToPos(animation, constraint, constraintIndex, animation.Duration, false, ref loopPos);
ApplyConstraintToPos(animation, constraint, constraintIndex, 0, false, ref zeroPos);
}
currentDelta += loopPos - zeroPos;
}
UpdateLastConstraintPos(transformConstraintsItems);
return currentDelta;
}
public float GetAnimationRootMotionRotation (Animation animation) {
return GetAnimationRootMotionRotation(0, animation.Duration, animation);
}
public float GetAnimationRootMotionRotation (float startTime, float endTime,
Animation animation) {
if (startTime == endTime)
return 0;
RotateTimeline rotateTimeline = animation.FindTimelineForBone(rootMotionBoneIndex);
// Non-looped base
float endRotation = 0;
float startRotation = 0;
if (rotateTimeline != null) {
endRotation = rotateTimeline.Evaluate(endTime);
startRotation = rotateTimeline.Evaluate(startTime);
}
TransformConstraint[] transformConstraintsItems = skeletonComponent.Skeleton.TransformConstraints.Items;
foreach (int constraintIndex in this.transformConstraintIndices) {
TransformConstraint constraint = transformConstraintsItems[constraintIndex];
ApplyConstraintToRotation(animation, constraint, constraintIndex, endTime, false, ref endRotation);
ApplyConstraintToRotation(animation, constraint, constraintIndex, startTime, true, ref startRotation);
}
float currentDelta = endRotation - startRotation;
// Looped additions
if (startTime > endTime) {
float loopRotation = 0;
float zeroPos = 0;
if (rotateTimeline != null) {
loopRotation = rotateTimeline.Evaluate(animation.Duration);
zeroPos = rotateTimeline.Evaluate(0);
}
foreach (int constraintIndex in this.transformConstraintIndices) {
TransformConstraint constraint = transformConstraintsItems[constraintIndex];
ApplyConstraintToRotation(animation, constraint, constraintIndex, animation.Duration, false, ref loopRotation);
ApplyConstraintToRotation(animation, constraint, constraintIndex, 0, false, ref zeroPos);
}
currentDelta += loopRotation - zeroPos;
}
UpdateLastConstraintRotation(transformConstraintsItems);
return currentDelta;
}
void ApplyConstraintToPos (Animation animation, TransformConstraint constraint,
int constraintIndex, float time, bool useLastConstraintPos, ref Vector2 pos) {
TransformConstraintTimeline timeline = animation.FindTransformConstraintTimeline(constraintIndex);
if (timeline == null)
return;
Vector2 mixXY = timeline.EvaluateTranslateXYMix(time);
Vector2 invMixXY = timeline.EvaluateTranslateXYMix(time);
Vector2 constraintPos;
if (useLastConstraintPos)
constraintPos = transformConstraintLastPos[GetConstraintLastPosIndex(constraintIndex)];
else {
Bone targetBone = constraint.Target;
constraintPos = new Vector2(targetBone.X, targetBone.Y);
}
pos = new Vector2(
pos.x * invMixXY.x + constraintPos.x * mixXY.x,
pos.y * invMixXY.y + constraintPos.y * mixXY.y);
}
void ApplyConstraintToRotation (Animation animation, TransformConstraint constraint,
int constraintIndex, float time, bool useLastConstraintRotation, ref float rotation) {
TransformConstraintTimeline timeline = animation.FindTransformConstraintTimeline(constraintIndex);
if (timeline == null)
return;
float mixRotate = timeline.EvaluateRotateMix(time);
float invMixRotate = timeline.EvaluateRotateMix(time);
float constraintRotation;
if (useLastConstraintRotation)
constraintRotation = transformConstraintLastRotation[GetConstraintLastPosIndex(constraintIndex)];
else {
Bone targetBone = constraint.Target;
constraintRotation = targetBone.Rotation;
}
rotation = rotation * invMixRotate + constraintRotation * mixRotate;
}
void UpdateLastConstraintPos (TransformConstraint[] transformConstraintsItems) {
foreach (int constraintIndex in this.transformConstraintIndices) {
TransformConstraint constraint = transformConstraintsItems[constraintIndex];
Bone targetBone = constraint.Target;
transformConstraintLastPos[GetConstraintLastPosIndex(constraintIndex)] = new Vector2(targetBone.X, targetBone.Y);
}
}
void UpdateLastConstraintRotation (TransformConstraint[] transformConstraintsItems) {
foreach (int constraintIndex in this.transformConstraintIndices) {
TransformConstraint constraint = transformConstraintsItems[constraintIndex];
Bone targetBone = constraint.Target;
transformConstraintLastRotation[GetConstraintLastPosIndex(constraintIndex)] = targetBone.Rotation;
}
}
public RootMotionInfo GetAnimationRootMotionInfo (Animation animation, float currentTime) {
RootMotionInfo rootMotion = new RootMotionInfo();
float duration = animation.Duration;
float mid = duration * 0.5f;
rootMotion.timeIsPastMid = currentTime > mid;
TranslateTimeline timeline = animation.FindTranslateTimelineForBone(rootMotionBoneIndex);
if (timeline != null) {
rootMotion.start = timeline.Evaluate(0);
rootMotion.current = timeline.Evaluate(currentTime);
rootMotion.mid = timeline.Evaluate(mid);
rootMotion.end = timeline.Evaluate(duration);
return rootMotion;
}
TranslateXTimeline xTimeline = animation.FindTimelineForBone(rootMotionBoneIndex);
TranslateYTimeline yTimeline = animation.FindTimelineForBone(rootMotionBoneIndex);
if (xTimeline != null || yTimeline != null) {
rootMotion.start = TimelineExtensions.Evaluate(xTimeline, yTimeline, 0);
rootMotion.current = TimelineExtensions.Evaluate(xTimeline, yTimeline, currentTime);
rootMotion.mid = TimelineExtensions.Evaluate(xTimeline, yTimeline, mid);
rootMotion.end = TimelineExtensions.Evaluate(xTimeline, yTimeline, duration);
return rootMotion;
}
return rootMotion;
}
int GetConstraintLastPosIndex (int constraintIndex) {
ExposedList constraints = skeletonComponent.Skeleton.TransformConstraints;
return transformConstraintIndices.FindIndex(addedIndex => addedIndex == constraintIndex);
}
void FindTransformConstraintsAffectingBone () {
ExposedList constraints = skeletonComponent.Skeleton.TransformConstraints;
TransformConstraint[] constraintsItems = constraints.Items;
for (int i = 0, n = constraints.Count; i < n; ++i) {
TransformConstraint constraint = constraintsItems[i];
if (constraint.Bones.Contains(rootMotionBone)) {
transformConstraintIndices.Add(i);
Bone targetBone = constraint.Target;
Vector2 constraintPos = new Vector2(targetBone.X, targetBone.Y);
transformConstraintLastPos.Add(constraintPos);
transformConstraintLastRotation.Add(targetBone.Rotation);
}
}
}
Vector2 GetTimelineMovementDelta (float startTime, float endTime,
TranslateXTimeline xTimeline, TranslateYTimeline yTimeline, Animation animation) {
Vector2 currentDelta;
if (startTime > endTime) // Looped
currentDelta =
(TimelineExtensions.Evaluate(xTimeline, yTimeline, animation.Duration)
- TimelineExtensions.Evaluate(xTimeline, yTimeline, startTime))
+ (TimelineExtensions.Evaluate(xTimeline, yTimeline, endTime)
- TimelineExtensions.Evaluate(xTimeline, yTimeline, 0));
else if (startTime != endTime) // Non-looped
currentDelta = TimelineExtensions.Evaluate(xTimeline, yTimeline, endTime)
- TimelineExtensions.Evaluate(xTimeline, yTimeline, startTime);
else
currentDelta = Vector2.zero;
return currentDelta;
}
void GatherTopLevelBones () {
topLevelBones.Clear();
Skeleton skeleton = skeletonComponent.Skeleton;
foreach (Bone bone in skeleton.Bones) {
if (bone.Parent == null)
topLevelBones.Add(bone);
}
}
void HandleUpdateLocal (ISkeletonAnimation animatedSkeletonComponent) {
if (!this.isActiveAndEnabled)
return; // Root motion is only applied when component is enabled.
Vector2 boneLocalDelta = CalculateAnimationsMovementDelta();
Vector2 parentBoneScale;
Vector2 totalScale;
Vector2 skeletonTranslationDelta = GetSkeletonSpaceMovementDelta(boneLocalDelta, out parentBoneScale, out totalScale);
float skeletonRotationDelta = 0;
if (transformRotation) {
float boneLocalDeltaRotation = CalculateAnimationsRotationDelta();
boneLocalDeltaRotation *= rootMotionScaleRotation;
skeletonRotationDelta = GetSkeletonSpaceRotationDelta(boneLocalDeltaRotation, totalScale);
}
bool usesFixedUpdate = SkeletonAnimationUsesFixedUpdate;
ApplyRootMotion(skeletonTranslationDelta, skeletonRotationDelta, parentBoneScale, usesFixedUpdate);
if (usesFixedUpdate)
PhysicsUpdate(usesFixedUpdate);
}
void ApplyRootMotion (Vector2 skeletonTranslationDelta, float skeletonRotationDelta, Vector2 parentBoneScale,
bool skeletonAnimationUsesFixedUpdate) {
// 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);
if (skeletonRotationDelta != 0.0f) {
if (rigidBody != null) {
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;
}
}
} 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);
}
}
void ApplyTransformConstraints () {
rootMotionBone.AX = rootMotionBone.X;
rootMotionBone.AY = rootMotionBone.Y;
rootMotionBone.AppliedRotation = rootMotionBone.Rotation;
TransformConstraint[] transformConstraintsItems = skeletonComponent.Skeleton.TransformConstraints.Items;
foreach (int constraintIndex in this.transformConstraintIndices) {
TransformConstraint constraint = transformConstraintsItems[constraintIndex];
// apply the constraint and sets Bone.ax, Bone.ay and Bone.arotation values.
/// Update is based on Bone.x, Bone.y and Bone.rotation, so skeleton.UpdateWorldTransform()
/// can be called afterwards without having a different starting point.
constraint.Update(Skeleton.Physics.None);
}
}
Vector2 GetScaleAffectingRootMotion () {
Vector2 parentBoneScale;
return GetScaleAffectingRootMotion(out parentBoneScale);
}
Vector2 GetScaleAffectingRootMotion (out Vector2 parentBoneScale) {
Skeleton skeleton = skeletonComponent.Skeleton;
Vector2 totalScale = Vector2.one;
totalScale.x *= skeleton.ScaleX;
totalScale.y *= skeleton.ScaleY;
parentBoneScale = Vector2.one;
Bone scaleBone = rootMotionBone;
while ((scaleBone = scaleBone.Parent) != null) {
#if USE_APPLIED_PARENT_SCALE
parentBoneScale.x *= scaleBone.AScaleX;
parentBoneScale.y *= scaleBone.AScaleY;
#else
parentBoneScale.x *= scaleBone.ScaleX;
parentBoneScale.y *= scaleBone.ScaleY;
#endif
}
totalScale = Vector2.Scale(totalScale, parentBoneScale);
totalScale *= AdditionalScale;
return totalScale;
}
Vector2 GetSkeletonSpaceMovementDelta (Vector2 boneLocalDelta, out Vector2 parentBoneScale, out Vector2 totalScale) {
Vector2 skeletonDelta = boneLocalDelta;
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;
}
float GetSkeletonSpaceRotationDelta (float boneLocalDelta, Vector2 totalScaleAffectingRootMotion) {
float rotationSign = totalScaleAffectingRootMotion.x * totalScaleAffectingRootMotion.y > 0 ? 1 : -1;
return rotationSign * boneLocalDelta;
}
void SetEffectiveBoneOffsetsTo (Vector2 displacementSkeletonSpace, float rotationSkeletonSpace, Vector2 parentBoneScale) {
ApplyTransformConstraints();
// Move top level bones in opposite direction of the root motion bone
Skeleton skeleton = skeletonComponent.Skeleton;
foreach (Bone topLevelBone in topLevelBones) {
if (topLevelBone == rootMotionBone) {
if (transformPositionX) topLevelBone.X = displacementSkeletonSpace.x / skeleton.ScaleX;
if (transformPositionY) topLevelBone.Y = displacementSkeletonSpace.y / skeleton.ScaleY;
if (transformRotation) {
float rotationSign = skeleton.ScaleX * skeleton.ScaleY > 0 ? 1 : -1;
topLevelBone.Rotation = rotationSign * rotationSkeletonSpace;
}
} else {
bool useAppliedTransform = transformConstraintIndices.Count > 0;
float rootMotionBoneX = useAppliedTransform ? rootMotionBone.AX : rootMotionBone.X;
float rootMotionBoneY = useAppliedTransform ? rootMotionBone.AY : rootMotionBone.Y;
float offsetX = (initialOffset.x - rootMotionBoneX) * parentBoneScale.x;
float offsetY = (initialOffset.y - rootMotionBoneY) * parentBoneScale.y;
if (transformPositionX) topLevelBone.X = (displacementSkeletonSpace.x / skeleton.ScaleX) + offsetX;
if (transformPositionY) topLevelBone.Y = (displacementSkeletonSpace.y / skeleton.ScaleY) + offsetY;
if (transformRotation) {
float rootMotionBoneRotation = useAppliedTransform ? rootMotionBone.AppliedRotation : rootMotionBone.Rotation;
float parentBoneRotationSign = (parentBoneScale.x * parentBoneScale.y > 0 ? 1 : -1);
float offsetRotation = (initialOffsetRotation - rootMotionBoneRotation) * parentBoneRotationSign;
float skeletonRotationSign = skeleton.ScaleX * skeleton.ScaleY > 0 ? 1 : -1;
topLevelBone.Rotation = (rotationSkeletonSpace * skeletonRotationSign) + offsetRotation;
}
}
}
}
void ClearEffectiveBoneOffsets (Vector2 parentBoneScale) {
SetEffectiveBoneOffsetsTo(Vector2.zero, 0, parentBoneScale);
}
void ClearRigidbodyTempMovement () {
rigidbodyDisplacement = Vector2.zero;
tempSkeletonDisplacement = Vector2.zero;
rigidbodyLocalRotation = Quaternion.identity;
rigidbody2DRotation = 0;
tempSkeletonRotation = 0;
}
}
}