745 lines
33 KiB
C#
745 lines
33 KiB
C#
/******************************************************************************
|
|
* 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 {
|
|
|
|
/// <summary>
|
|
/// Base class for skeleton root motion components.
|
|
/// </summary>
|
|
[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;
|
|
/// <summary>Skeleton space X translation per skeleton space Y translation root motion.</summary>
|
|
public float rootMotionTranslateXPerY = 0;
|
|
/// <summary>Skeleton space Y translation per skeleton space X translation root motion.</summary>
|
|
public float rootMotionTranslateYPerX = 0;
|
|
|
|
[Header("Optional")]
|
|
public Rigidbody2D rigidBody2D;
|
|
public bool applyRigidbody2DGravity = false;
|
|
public Rigidbody rigidBody;
|
|
|
|
/// <summary>Delegate type for customizing application of rootmotion.
|
|
public delegate void RootMotionDelegate (SkeletonRootMotionBase component, Vector2 translation, float rotation);
|
|
/// <summary>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 <see cref="disableOnOverride"/>)
|
|
/// to either Transform or Rigidbody.
|
|
/// When <see cref="SkeletonAnimation.UpdateTiming"/> is set to <see cref="UpdateTiming.InUpdate"/>, multiple
|
|
/// animation frames might take place before <c>FixedUpdate</c> is called once.
|
|
/// The callback parameters <c>translation</c> and <c>rotation</c> are filled out with
|
|
/// this animation frame's skeleton-space root-motion (not cumulated). You can use
|
|
/// e.g. <c>transform.TransformVector()</c> to transform skeleton-space root-motion to world space.
|
|
/// </summary>
|
|
/// <seealso cref="PhysicsUpdateRootMotionOverride"/>
|
|
public event RootMotionDelegate ProcessRootMotionOverride;
|
|
/// <summary>This callback can be used to apply root-motion in a custom way. It is raised in FixedUpdate
|
|
/// after (when <see cref="disableOnOverride"/> is set to false) or instead of when root-motion
|
|
/// would be applied at the Rigidbody.
|
|
/// When <see cref="SkeletonAnimation.UpdateTiming"/> is set to <see cref="UpdateTiming.InUpdate"/>, multiple
|
|
/// animation frames might take place before before <c>FixedUpdate</c> is called once.
|
|
/// The callback parameters <c>translation</c> and <c>rotation</c> are filled out with the
|
|
/// (cumulated) skeleton-space root-motion since the the last <c>FixedUpdate</c> call. You can use
|
|
/// e.g. <c>transform.TransformVector()</c> to transform skeleton-space root-motion to world space.
|
|
/// </summary>
|
|
/// <seealso cref="ProcessRootMotionOverride"/>
|
|
public event RootMotionDelegate PhysicsUpdateRootMotionOverride;
|
|
/// <summary>When true, root-motion is not applied to the Transform or Rigidbody.
|
|
/// Otherwise the delegate callbacks are issued additionally.</summary>
|
|
public bool disableOnOverride = true;
|
|
|
|
public Bone RootMotionBone { get { return rootMotionBone; } }
|
|
|
|
public bool UsesRigidbody {
|
|
get { return rigidBody != null || rigidBody2D != null; }
|
|
}
|
|
|
|
/// <summary>Root motion translation that has been applied in the preceding <c>FixedUpdate</c> call
|
|
/// if a rigidbody is assigned at either <c>rigidbody</c> or <c>rigidbody2D</c>.
|
|
/// Returns <c>Vector2.zero</c> when <c>rigidbody</c> and <c>rigidbody2D</c> are null.
|
|
/// This can be necessary when multiple scripts call <c>Rigidbody2D.MovePosition</c>,
|
|
/// where the last call overwrites the effect of preceding ones.</summary>
|
|
public Vector2 PreviousRigidbodyRootMotion2D {
|
|
get { return new Vector2(previousRigidbodyRootMotion.x, previousRigidbodyRootMotion.y); }
|
|
}
|
|
|
|
/// <summary>Root motion translation that has been applied in the preceding <c>FixedUpdate</c> call
|
|
/// if a rigidbody is assigned at either <c>rigidbody</c> or <c>rigidbody2D</c>.
|
|
/// Returns <c>Vector3.zero</c> when <c>rigidbody</c> and <c>rigidbody2D</c> are null.</summary>
|
|
public Vector3 PreviousRigidbodyRootMotion3D {
|
|
get { return previousRigidbodyRootMotion; }
|
|
}
|
|
|
|
/// <summary>Additional translation to add to <c>Rigidbody2D.MovePosition</c>
|
|
/// called in FixedUpdate. This can be necessary when multiple scripts call
|
|
/// <c>MovePosition</c>, where the last call overwrites the effect of preceding ones.
|
|
/// Has no effect if <c>rigidBody2D</c> is null.</summary>
|
|
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<int> transformConstraintIndices = new List<int>();
|
|
protected List<Vector2> transformConstraintLastPos = new List<Vector2>();
|
|
protected List<float> transformConstraintLastRotation = new List<float>();
|
|
protected List<Bone> topLevelBones = new List<Bone>();
|
|
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<ISkeletonComponent>();
|
|
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<SkeletonUtility>();
|
|
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<Rigidbody2D>();
|
|
if (!rigidBody2D)
|
|
rigidBody = this.GetComponent<Rigidbody>();
|
|
|
|
if (!rigidBody2D && !rigidBody) {
|
|
rigidBody2D = this.GetComponentInParent<Rigidbody2D>();
|
|
if (!rigidBody2D)
|
|
rigidBody = this.GetComponentInParent<Rigidbody>();
|
|
}
|
|
}
|
|
|
|
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<ISkeletonComponent>();
|
|
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<TranslateXTimeline>(rootMotionBoneIndex);
|
|
TranslateYTimeline yTimeline = animation.FindTimelineForBone<TranslateYTimeline>(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<RotateTimeline>(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<TranslateXTimeline>(rootMotionBoneIndex);
|
|
TranslateYTimeline yTimeline = animation.FindTimelineForBone<TranslateYTimeline>(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<TransformConstraint> constraints = skeletonComponent.Skeleton.TransformConstraints;
|
|
return transformConstraintIndices.FindIndex(addedIndex => addedIndex == constraintIndex);
|
|
}
|
|
|
|
void FindTransformConstraintsAffectingBone () {
|
|
ExposedList<TransformConstraint> 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;
|
|
}
|
|
}
|
|
}
|