/****************************************************************************** * 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; } } }