diff --git a/Source/OLSAnimation/Private/Libraries/OLSLocomotionBPLibrary.cpp b/Source/OLSAnimation/Private/Libraries/OLSLocomotionBPLibrary.cpp index 7763f6c..1ad2519 100644 --- a/Source/OLSAnimation/Private/Libraries/OLSLocomotionBPLibrary.cpp +++ b/Source/OLSAnimation/Private/Libraries/OLSLocomotionBPLibrary.cpp @@ -12,116 +12,156 @@ DEFINE_LOG_CATEGORY_STATIC(LogOLSLocomotionLibrary, Verbose, All); float UOLSLocomotionBPLibrary::FindPivotTime(const UAnimSequenceBase* animSequence, const float sampleRate) { - if (animSequence) - { - const float animLength = animSequence->GetPlayLength(); - const float sampleDeltaTime = 1 / sampleRate; - float currentAnimTime = 0.f; - float lastTime = 0.f; - float nextTime = currentAnimTime + sampleDeltaTime; - FVector currentLocation = animSequence->ExtractRootMotionFromRange(currentAnimTime, nextTime). - GetTranslation().GetSafeNormal2D(); - while (nextTime < animLength) - { - const FRotator currentRotation = animSequence->ExtractRootMotionFromRange(0.0f, currentAnimTime). - GetRotation().Rotator(); - const FVector lastLocation = currentRotation.RotateVector( - animSequence->ExtractRootMotionFromRange( - currentAnimTime, nextTime).GetTranslation().GetSafeNormal2D()); - if ((currentLocation.Dot(lastLocation) < 0 && currentLocation.SquaredLength() > 0) || ( - FMath::IsNearlyZero(lastLocation.SquaredLength()) && currentLocation.SquaredLength() > 0)) - { - return currentAnimTime; - } + if (animSequence) + { + const float animLength = animSequence->GetPlayLength(); // Get the total duration of the animation sequence. + const float sampleDeltaTime = 1 / sampleRate; // Calculate the time interval between each sample based on the sample rate. + + float currentAnimTime = 0.f; // Initialize the current animation time. + float lastTime = 0.f; // Store the last sampled time. + float nextTime = currentAnimTime + sampleDeltaTime; // Calculate the next time point for sampling. + + // Extract and normalize the initial root motion translation vector. + FVector currentLocation = animSequence->ExtractRootMotionFromRange(currentAnimTime, nextTime) + .GetTranslation().GetSafeNormal2D(); - if (FMath::IsNearlyZero(currentLocation.Length())) - { - currentLocation = lastLocation; - } + while (nextTime < animLength) // Loop through the animation until the end. + { + // Extract the current rotation based on the root motion from the start to the current time. + const FRotator currentRotation = animSequence->ExtractRootMotionFromRange(0.0f, currentAnimTime) + .GetRotation().Rotator(); + + // Apply the current rotation to the translation vector and normalize. + const FVector lastLocation = currentRotation.RotateVector( + animSequence->ExtractRootMotionFromRange(currentAnimTime, nextTime) + .GetTranslation().GetSafeNormal2D()); + + // Detect a pivot point if the dot product is negative (indicating a direction change). + if ((currentLocation.Dot(lastLocation) < 0 && currentLocation.SquaredLength() > 0) || + (FMath::IsNearlyZero(lastLocation.SquaredLength()) && currentLocation.SquaredLength() > 0)) + { + return currentAnimTime; // Return the detected pivot time. + } - lastTime = FMath::Clamp(lastTime + sampleDeltaTime, 0.0f, animLength); - currentAnimTime = FMath::Clamp(currentAnimTime + sampleDeltaTime, 0.0f, animLength); - nextTime = FMath::Clamp(nextTime + sampleDeltaTime, 0.0f, animLength); - } - } + // Handle the case where the current location length is nearly zero. + if (FMath::IsNearlyZero(currentLocation.Length())) + { + currentLocation = lastLocation; // Update the current location for the next iteration. + } - return 0.f; + // Advance to the next sample time, clamping to ensure it doesn't exceed the animation length. + lastTime = FMath::Clamp(lastTime + sampleDeltaTime, 0.0f, animLength); + currentAnimTime = FMath::Clamp(currentAnimTime + sampleDeltaTime, 0.0f, animLength); + nextTime = FMath::Clamp(nextTime + sampleDeltaTime, 0.0f, animLength); + } + } + + return 0.f; // Return 0 if no pivot is detected or if the input animation sequence is invalid. } float UOLSLocomotionBPLibrary::GetCurveValueAtTime(const UAnimSequenceBase* animSequence, const float time, const FName& curveName) { + // Initialize buffer access for the specified curve in the given animation sequence. FAnimCurveBufferAccess bufferCurveAccess(animSequence, curveName); + + // Validate that the curve data is accessible. if (bufferCurveAccess.IsValid()) { + // Clamp the time to ensure it's within the valid range of the animation length. const float clampedTime = FMath::Clamp(time, 0.f, animSequence->GetPlayLength()); + + // Ensure the animation has at least 3 sampled keys for evaluation (2 keys are needed for interpolation). if (animSequence->GetNumberOfSampledKeys() > 2) { + // Evaluate the curve data at the specified time and return the result. return animSequence->EvaluateCurveData(curveName, clampedTime); } } + // Return 0 if the curve is invalid or the animation has insufficient sampled keys. return 0.f; } -float UOLSLocomotionBPLibrary::GetTimeAtDistance(const UAnimSequenceBase* animSequence, - const float& distance, FName curveName) +float UOLSLocomotionBPLibrary::GetTimeAtCurveValue(const UAnimSequenceBase* animSequence, + const float& curveValue, FName curveName) { - FAnimCurveBufferAccess bufferCurveAccess(animSequence, curveName); - if (bufferCurveAccess.IsValid()) - { - const int32 numKeys = bufferCurveAccess.GetNumSamples(); - if (numKeys < 2) - { - return 0.f; - } + // Initialize buffer access for the specified curve in the given animation sequence. + FAnimCurveBufferAccess bufferCurveAccess(animSequence, curveName); - int32 first = 1; - int32 last = numKeys - 1; - int32 count = last - first; + // Validate the curve data. + if (bufferCurveAccess.IsValid()) + { + const int32 numKeys = bufferCurveAccess.GetNumSamples(); // Retrieve the total number of keyframes/samples. - while (count > 0) - { - int32 step = count / 2; - int32 middle = first + step; + // Ensure there are at least two keyframes for interpolation. + if (numKeys < 2) + { + return 0.f; // Return 0 if not enough data points. + } - if (distance > bufferCurveAccess.GetValue(middle)) - { - first = middle + 1; - count -= step + 1; - } - else - { - count = step; - } - } + // Initialize binary search variables. + int32 first = 1; // Start at the second keyframe. + int32 last = numKeys - 1; // Index of the last keyframe. + int32 count = last - first; // Number of keyframes to search through. - const float keyAValue = bufferCurveAccess.GetValue(first - 1); - const float keyBValue = bufferCurveAccess.GetValue(first); - const float diff = keyBValue - keyAValue; - const float alpha = !FMath::IsNearlyZero(diff) ? ((distance - keyAValue) / diff) : 0.f; + // Perform a binary search to locate the interval containing the curve value. + while (count > 0) + { + int32 step = count / 2; // Calculate the midpoint step. + int32 middle = first + step; // Determine the middle keyframe. - const float keyATime = bufferCurveAccess.GetTime(first - 1); - const float keyBTime = bufferCurveAccess.GetTime(first); - return FMath::Lerp(keyATime, keyBTime, alpha); - } + // Adjust the search range based on the target curve value. + if (curveValue > bufferCurveAccess.GetValue(middle)) + { + first = middle + 1; // Move the search to the right half. + count -= step + 1; // Update the remaining count. + } + else + { + count = step; // Narrow the search to the left half. + } + } - return 0.f; + // Retrieve values at the keyframes surrounding the target value. + const float keyAValue = bufferCurveAccess.GetValue(first - 1); + const float keyBValue = bufferCurveAccess.GetValue(first); + const float diff = keyBValue - keyAValue; // Calculate the difference between the values. + + // Calculate the interpolation factor (alpha) based on the target value. + const float alpha = !FMath::IsNearlyZero(diff) ? ((curveValue - keyAValue) / diff) : 0.f; + + // Retrieve the corresponding times for the surrounding keyframes. + const float keyATime = bufferCurveAccess.GetTime(first - 1); + const float keyBTime = bufferCurveAccess.GetTime(first); + + // Linearly interpolate between the keyframe times to estimate the target time. + return FMath::Lerp(keyATime, keyBTime, alpha); + } + + return 0.f; // Return 0 if the curve is invalid or the target value is not found. } -float UOLSLocomotionBPLibrary::GetDistanceRange(const UAnimSequenceBase* animSequence, const FName& curveName) +float UOLSLocomotionBPLibrary::GetCurveValuesRange(const UAnimSequenceBase* animSequence, const FName& curveName) { + // Initialize a buffer to access the curve data within the specified animation sequence. FAnimCurveBufferAccess bufferCurveAccess(animSequence, curveName); + + // Check if the curve data is valid and accessible. if (bufferCurveAccess.IsValid()) { - const int32 numSamples = bufferCurveAccess.GetNumSamples(); + const int32 numSamples = bufferCurveAccess.GetNumSamples(); // Get the total number of samples in the curve. + + // Ensure there are at least two samples to calculate a meaningful range. if (numSamples >= 2) { + // Calculate the range by subtracting the first sample value from the last sample value. return (bufferCurveAccess.GetValue(numSamples - 1) - bufferCurveAccess.GetValue(0)); } } - return 0.f; + + return 0.f; // Return 0 if the curve is invalid or does not have enough data points. } float UOLSLocomotionBPLibrary::GetTimeAfterDistanceTraveled(const UAnimSequenceBase* animSequence, @@ -133,7 +173,7 @@ float UOLSLocomotionBPLibrary::GetTimeAfterDistanceTraveled(const UAnimSequenceB if (animSequence) { // Avoid infinite loops if the animation doesn't cover any distance. - if (!FMath::IsNearlyZero(GetDistanceRange(animSequence, curveName))) + if (!FMath::IsNearlyZero(GetCurveValuesRange(animSequence, curveName))) { float accumulatedDistance = 0.f; @@ -285,7 +325,7 @@ FSequenceEvaluatorReference UOLSLocomotionBPLibrary::DistanceMatchToTarget(const USequenceEvaluatorLibrary::GetAccumulatedTime(sequenceEvaluator), curveName) > stopDistanceThreshHold && !shouldDistanceMatchStop) { - const float newTime = GetTimeAtDistance(animSequence, -distanceToTarget, curveName); + const float newTime = GetTimeAtCurveValue(animSequence, -distanceToTarget, curveName); if (!inSequenceEvaluator.SetExplicitTime(newTime)) { UE_LOG(LogOLSLocomotionLibrary, Warning, diff --git a/Source/OLSAnimation/Public/Libraries/OLSLocomotionBPLibrary.h b/Source/OLSAnimation/Public/Libraries/OLSLocomotionBPLibrary.h index 64c55cb..e06be97 100644 --- a/Source/OLSAnimation/Public/Libraries/OLSLocomotionBPLibrary.h +++ b/Source/OLSAnimation/Public/Libraries/OLSLocomotionBPLibrary.h @@ -16,27 +16,96 @@ class OLSANIMATION_API UOLSLocomotionBPLibrary : public UBlueprintFunctionLibrar { GENERATED_BODY() -public: +public: // ~ Helpers ~ // - UFUNCTION(BlueprintCallable, Category = "OLS|Function Library", meta=(BlueprintThreadSafe)) + /** + * Finds the time within an animation sequence when a character's root motion changes direction, commonly known as a "pivot." + * + * @param animSequence Pointer to the animation sequence being analyzed. The function extracts root motion data from this sequence. + * @param sampleRate The frequency (in Hz) at which the animation is sampled. Higher values increase accuracy but may impact performance. + * + * @return The time (in seconds) within the animation sequence when a pivot occurs. Returns 0 if no pivot is detected or the input is invalid. + * + * @note This function is useful for identifying key moments in animations where directional changes occur, + * such as during character turns or sharp movements, ensuring smooth transitions or special handling. + */ static float FindPivotTime(const UAnimSequenceBase* animSequence, const float sampleRate); - UFUNCTION(BlueprintCallable, Category = "OLS|Function Library", meta=(BlueprintThreadSafe)) + /** + * Retrieves the value of a specified curve at a given time within an animation sequence. + * + * @param animSequence Pointer to the animation sequence containing the curve data. + * @param time The time (in seconds) at which to evaluate the curve value. + * @param curveName The name of the curve to evaluate. + * + * @return The curve value at the specified time. Returns 0 if the curve is invalid or the time is out of range. + * + * @note The time is clamped to ensure it falls within the animation's playback range, and the function requires at least 2 sampled keys in the animation for curve evaluation. + */ static float GetCurveValueAtTime(const UAnimSequenceBase* animSequence, const float time, const FName& curveName); - - UFUNCTION(BlueprintCallable, Category = "OLS|Function Library", meta=(BlueprintThreadSafe)) - static float GetTimeAtDistance(const UAnimSequenceBase* animSequence, const float& distance, FName curveName); - - UFUNCTION(BlueprintCallable, Category = "OLS|Function Library", meta=(BlueprintThreadSafe)) - static float GetDistanceRange(const UAnimSequenceBase* animSequence, const FName& curveName); - + + /** + * Retrieves the time within an animation sequence at which a specified curve reaches a given value. + * + * @param animSequence Pointer to the animation sequence containing the curve data. + * @param curveValue The target value to locate within the curve. + * @param curveName The name of the curve being evaluated. + * + * @return The interpolated time at which the curve reaches the specified value. Returns 0 if the curve is invalid or the value cannot be found. + * + * @note This function uses binary search to efficiently locate the curve value and linearly interpolates between keyframes for precision. + */ + static float GetTimeAtCurveValue(const UAnimSequenceBase* animSequence, const float& curveValue, FName curveName); + + /** + * Calculates the range of values for a specified animation curve within an animation sequence. + * + * @param animSequence Pointer to the animation sequence containing the curve. This sequence provides the curve data. + * @param curveName The name of the curve whose value range is to be calculated. + * + * @return The difference between the first and last values of the specified curve. Returns 0 if the curve is invalid or has insufficient data. + * + * @note This function is useful for determining the total change in a curve over the duration of an animation, + * which can be critical for understanding motion characteristics or driving procedural animations. + */ + static float GetCurveValuesRange(const UAnimSequenceBase* animSequence, const FName& curveName); + +public: + + /** + * Advances the animation time from the current position to a new time, ensuring the root motion covers the specified distance traveled. + * + * @param animSequence Pointer to the animation sequence being evaluated. Contains the root motion and curve data. + * @param currentTime The current time within the animation sequence, serving as the starting point for advancement. + * @param distanceTraveled The desired distance to advance within the animation, calculated based on the root motion curve. + * @param curveName The name of the curve representing root motion distance in the animation sequence. + * @param shouldAllowLooping Specifies whether the animation should loop if the new time exceeds the sequence length. + * + * @return The new animation time after advancing the desired distance along the root motion curve. + * + * @note This function ensures that the visual progression of the animation matches the actual distance traveled, + * which is particularly useful for root motion-based locomotion systems. + */ UFUNCTION(BlueprintCallable, Category = "OLS|Function Library", meta=(BlueprintThreadSafe)) static float GetTimeAfterDistanceTraveled(const UAnimSequenceBase* animSequence, float currentTime, float distanceTraveled, FName curveName, const bool shouldAllowLooping); - + + /** + * Advances the animation time based on the distance traveled, adjusting the play rate to synchronize the animation with movement. + * + * @param outDesiredPlayRate Output parameter that will contain the calculated play rate needed to match the distance traveled. + * @param updateContext Provides context for the current animation update, including delta time and other relevant information. + * @param sequenceEvaluator Reference to the sequence evaluator managing the current animation sequence. + * @param distanceTraveled The distance covered since the last frame. This value determines how much the animation time should advance. + * @param curveName The name of the curve used for distance matching. This curve defines how the animation corresponds to distance traveled. + * @param playRateClamp Optional parameter defining the minimum and maximum play rates. Clamps the effective play rate to prevent unrealistic values. + * Default value: FVector2D(0.75f, 1.25f). + * + * @return The updated sequence evaluator with the new animation state. + */ UFUNCTION(BlueprintCallable, Category = "OLS|Function Library", meta=(BlueprintThreadSafe)) static FSequenceEvaluatorReference AdvanceTimeByDistanceMatching(float& outDesiredPlayRate, const FAnimUpdateContext& updateContext,