// Copyright (C) 2023 Nicholas Maltbie // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and // associated documentation files (the "Software"), to deal in the Software without restriction, // including without limitation the rights to use, copy, modify, merge, publish, distribute, // sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY // CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using nickmaltbie.TestUtilsUnity; using UnityEngine; namespace nickmaltbie.OpenKCC.Animation { /// /// FootTarget for easily managing the player's current /// IK foot target position and weight. /// public class FootTarget { /// /// Threshold for animation weight value for changing between grounded and not grounded. /// public const float ThresholdGroundedStateChange = 0.5f; /// /// Threshold for animation IK weight value to consider the foot grounded. /// public const float ThresholdIKWeightGrounded = 0.95f; /// /// Threshold for animation IK weight value to consider the foot released. /// public const float ThresholdIKWeightReleased = 0.05f; /// /// Threshold fraction of animation at which a stride target can no longer be updated. /// public const float ThresholdFractionStrideNoUpdate = 0.15f; /// /// Name for animator curve to define the left foot IK weight. /// public const string LeftFootIKWeight = "LeftFootIKWeight"; /// /// Name for animator curve to define the right foot IK weight. /// public const string RightFootIKWeight = "RightFootIKWeight"; /// /// Smooth a value evenly between 0 and 1 /// using a sine function. /// /// Input value between [0,1] /// Smoothed value when placed on a smooth sine curve. public static float SmoothValue(float x) { if (x <= 0) { return 0; } else if (x >= 1) { return 1; } else { return (Mathf.Sin(Mathf.PI * (x - 0.5f)) + 1) * 0.5f; } } /// /// Animator for managing the player avatar. /// private Animator animator; /// /// Current velocity of the lerped foot IK weight for soothing. /// private float footIKWeightVelocityLerp; /// /// Position foot is being moved from when grounding. /// private Vector3 fromFootPosition = Vector3.zero; /// /// Rotation foot is being moved from when grounding. /// private Quaternion fromFootRotation = Quaternion.identity; /// /// Current velocity of the foot when smoothing foot position while raised. /// private Vector3 raisedFootVelocity = Vector3.zero; /// /// Unity service for managing time delta. /// public IUnityService unityService = UnityService.Instance; /// /// Construct a foot target with a given set of configurations /// and parameters. /// /// Foot being controlled. /// Animator managing avatar control. /// Height of stride for short steps. /// Time of stride for short steps. /// Height to raise foot of ground when placed. public FootTarget(Foot foot, Animator animator, float strideHeight, float strideTime, float footGroundedHeight) { Foot = foot; this.animator = animator; StrideHeight = strideHeight; FootGroundedHeight = footGroundedHeight; StrideTime = strideTime; } /// /// Position of foot with overlap corrected. /// public Vector3 OverlapCorrectedPosition { get; set; } /// /// Time foot has been overlapping with terrain. /// public float OverlapTime { get; set; } /// /// Which foot does this target correspond to. /// public Foot Foot { get; private set; } /// /// Time required to take a full stride for the foot. /// public float StrideTime { get; private set; } /// /// Height foot is lifted to mid stride. /// public float StrideHeight { get; private set; } /// /// Height foot is raised above the ground when taking a small step. /// public float FootGroundedHeight { get; private set; } /// /// Gets the time of the most recent stride's start. /// public float StrideStartTime { get; private set; } = Mathf.NegativeInfinity; /// /// Gets the target foot position for IK controls. /// public Vector3 TargetFootPosition { get; private set; } = Vector3.zero; /// /// Gets the forward vector for the foot rotation. /// public Vector3 FootForward { get; set; } /// /// Ground normal for surface player is standing on. /// public Vector3 GroundNormal { get; set; } /// /// Gets the target foot rotation for IK controls. /// public Quaternion TargetFootRotation { get; private set; } = Quaternion.identity; /// /// Gets the current weight value for IK foot placement. /// public float FootIKWeight { get; private set; } /// /// Gets the current state of the foot's control, either grounded /// or released. /// public FootState State { get; private set; } /// /// Gets the current object the foot is standing on. /// public GameObject Floor { get; private set; } /// /// Is the foot currently taking a small stride and needs to be bumped /// up off the ground. /// /// public bool UseBump { get; private set; } /// /// Gest the remaining time in the current stride based off the current /// game time. /// public float RemainingStrideTime => UseBump ? StrideStartTime + StrideTime - unityService.time : 0; /// /// Gets if this foot is currently mid stride. This foot /// will be considered mid stride if it's currently grounded /// and has remaining time in the current stride /// of if it was just released form the ground. This should /// be used to stop quickly changing state between grounded and /// not grounded causing the foot to jitter. /// public bool MidStride { get { if (State == FootState.Grounded && UseBump) { return RemainingStrideTime > 0; } else if (State == FootState.Grounded) { return FootIKWeight <= ThresholdIKWeightGrounded; } else { return FootIKWeight >= ThresholdIKWeightReleased; } } } /// /// Can the stride target for this foot be updated or snapped /// to a new position. /// /// True if the stride target can be updated, false otherwise. public bool CanUpdateStrideTarget() { if (State == FootState.Grounded && UseBump) { return RemainingStrideTime > StrideTime * ThresholdFractionStrideNoUpdate; } else if (State == FootState.Grounded) { return false; } else { return FootIKWeight <= ThresholdIKWeightReleased; } } /// /// Gets the current animation weight for the foot from the animator. /// /// Float animation weight for the selected foot. public float GetFootAnimationWeight() => Foot == Foot.LeftFoot ? animator.GetFloat(LeftFootIKWeight) : animator.GetFloat(RightFootIKWeight); /// /// Is the foot's animation weight over the grounded threshold. (should be grounded) /// /// True if the foot is over the grounded threshold, false otherwise. public bool OverGroundThreshold() => GetFootAnimationWeight() >= ThresholdGroundedStateChange; /// /// Is the foot's animation weight under the release threshold. (should be released) /// /// True if the foot is under the release threshold, false otherwise. public bool UnderReleaseThreshold() => GetFootAnimationWeight() <= ThresholdGroundedStateChange; /// /// Get the current foot IK target position in world space. This /// will account for any blending between the from and to position /// as well as any bump up for small strides. /// /// Foot IK Target position in world space. public Vector3 FootIKTargetPos() { if (RemainingStrideTime <= 0) { return TargetFootPosition + GroundNormal * FootGroundedHeight; } float fraction = 1 - Mathf.Clamp(RemainingStrideTime / StrideTime, 0, 1); var lerpPos = Vector3.Lerp(fromFootPosition, TargetFootPosition, SmoothValue(fraction)); Vector3 verticalOffset = UseBump ? Vector3.up * StrideHeight * Mathf.Sin(fraction * Mathf.PI) : Vector3.zero; return lerpPos + verticalOffset + GroundNormal * FootGroundedHeight; } /// /// Gets the current foot IK target rotation in world space. /// This will account for any blending between the from and to position. /// /// Foot IK Target rotation in world space. public Quaternion FootIKTargetRot() { if (RemainingStrideTime <= 0 || !UseBump) { return TargetFootRotation; } float fraction = 1 - Mathf.Clamp(RemainingStrideTime / StrideTime, 0, 1); return Quaternion.Lerp(fromFootRotation, TargetFootRotation, SmoothValue(fraction)); } /// /// Lerp the current foot weight based on delta time from the current /// target value to the new desired value based on foot state. /// public void LerpFootIKWeight() { float currentWeight = FootIKWeight; float targetWeight = State == FootState.Grounded ? 1.0f : 0.0f; FootIKWeight = Mathf.SmoothDamp(currentWeight, targetWeight, ref footIKWeightVelocityLerp, StrideTime, Mathf.Infinity, unityService.deltaTime); FootIKWeight = Mathf.Clamp(FootIKWeight, 0, 1); } /// /// Release the foot from the grounded state. /// public void ReleaseFoot() { State = FootState.Released; Floor = null; } /// /// Start a stride to either place the foot on the ground /// or move the foot a short distance. /// /// Desired target position of the foot. /// Desired target rotation of the foot. /// Object the foot is placed on. /// Forward vector for foot rotation. /// Normal vector for surface player is standing on. /// Is this a small bump from a currently grounded state. public void StartStride(Vector3 toPos, Quaternion toRot, GameObject floor, Vector3 footForward, Vector3 groundNormal, bool bumpStep) { fromFootPosition = TargetFootPosition; fromFootRotation = TargetFootRotation; TargetFootPosition = toPos; TargetFootRotation = toRot; Floor = floor; FootForward = footForward; GroundNormal = groundNormal; UseBump = bumpStep; State = FootState.Grounded; StrideStartTime = unityService.time; } /// /// Update the stride target but do not start a new step. /// /// Position to update stride target to. /// Rotation to update stride target to. /// New ground normal for foot position. /// Should this blend based on current delta time or simply be forced to a new position. public void UpdateStrideTarget(Vector3 toPos, Quaternion toRot, Vector3 groundNormal, bool force = false) { if (force) { TargetFootPosition = toPos; raisedFootVelocity = Vector3.zero; } else { TargetFootPosition = Vector3.SmoothDamp(TargetFootPosition, toPos, ref raisedFootVelocity, StrideTime, Mathf.Infinity, unityService.deltaTime); } GroundNormal = groundNormal; TargetFootRotation = toRot; } } }