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