diff --git a/Source/OLSAnimation/Private/Libraries/OLSLocomotionBPLibrary.cpp b/Source/OLSAnimation/Private/Libraries/OLSLocomotionBPLibrary.cpp index 50678db..39ef185 100644 --- a/Source/OLSAnimation/Private/Libraries/OLSLocomotionBPLibrary.cpp +++ b/Source/OLSAnimation/Private/Libraries/OLSLocomotionBPLibrary.cpp @@ -3,6 +3,184 @@ #include "Libraries/OLSLocomotionBPLibrary.h" +#include "SequencePlayerLibrary.h" +#include "Animation/AnimCurveCompressionCodec_UniformIndexable.h" +#include "Animation/AnimNode_SequencePlayer.h" +#include "AnimNodes/AnimNode_SequenceEvaluator.h" + +DEFINE_LOG_CATEGORY_STATIC(LogOLSLocomotionLibrary, Verbose, All); + +float UOLSLocomotionBPLibrary::GetDistanceCurveValueAtTime(const UAnimSequenceBase* animSequence, + const float time, + const FName& curveName) +{ + FAnimCurveBufferAccess bufferCurveAccess(animSequence, curveName); + if (bufferCurveAccess.IsValid()) + { + const float clampedTime = FMath::Clamp(time, 0.f, animSequence->GetPlayLength()); + if (animSequence->GetNumberOfSampledKeys() > 2) + { + return animSequence->EvaluateCurveData(curveName, clampedTime); + } + } + + return 0.f; +} + +float UOLSLocomotionBPLibrary::GetTimeAtDistance(const UAnimSequenceBase* animSequence, + const float& distance, FName curveName) +{ + FAnimCurveBufferAccess bufferCurveAccess(animSequence, curveName); + if (bufferCurveAccess.IsValid()) + { + const int32 numKeys = bufferCurveAccess.GetNumSamples(); + if (numKeys < 2) + { + return 0.f; + } + + int32 first = 1; + int32 last = numKeys - 1; + int32 count = last - first; + + while (count > 0) + { + int32 step = count / 2; + int32 middle = first + step; + + if (distance > bufferCurveAccess.GetValue(middle)) + { + first = middle + 1; + count -= step + 1; + } + else + { + count = step; + } + } + + 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; + + const float keyATime = bufferCurveAccess.GetTime(first - 1); + const float keyBTime = bufferCurveAccess.GetTime(first); + return FMath::Lerp(keyATime, keyBTime, alpha); + } + + return 0.f; +} + +FSequenceEvaluatorReference UOLSLocomotionBPLibrary::DistanceMatchToTarget(const FAnimUpdateContext& updateContext, + const FSequenceEvaluatorReference& + sequenceEvaluator, + float distanceToTarget, + bool shouldDistanceMatchStop, + float stopDistanceThreshHold, + float animEndTime, + FName curveName) +{ + sequenceEvaluator.CallAnimNodeFunction( + TEXT("DistanceMatchToTarget"), + [updateContext,sequenceEvaluator,distanceToTarget, shouldDistanceMatchStop,stopDistanceThreshHold,animEndTime, + curveName]( + FAnimNode_SequenceEvaluator& InSequenceEvaluator) + { + if (const UAnimSequenceBase* animSequence = InSequenceEvaluator.GetSequence()) + { + if (GetDistanceCurveValueAtTime(animSequence, + USequenceEvaluatorLibrary::GetAccumulatedTime(sequenceEvaluator), + curveName) > stopDistanceThreshHold && !shouldDistanceMatchStop) + { + const float newTime = GetTimeAtDistance(animSequence, -distanceToTarget, curveName); + if (!InSequenceEvaluator.SetExplicitTime(newTime)) + { + UE_LOG(LogOLSLocomotionLibrary, Warning, + TEXT( + "Could not set explicit time on sequence evaluator, value is not dynamic. Set it as Always Dynamic." + )); + } + } + else + { + USequenceEvaluatorLibrary::AdvanceTime(updateContext, sequenceEvaluator, 1.0f); + if (animEndTime > 0) + { + const float desiredTime = FMath::Clamp( + USequenceEvaluatorLibrary::GetAccumulatedTime(sequenceEvaluator), 0, animEndTime); + USequenceEvaluatorLibrary::SetExplicitTime(sequenceEvaluator, desiredTime); + } + } + } + else + { + UE_LOG(LogOLSLocomotionLibrary, Warning, + TEXT("Sequence evaluator does not have an anim sequence to play.")); + } + }); + + return sequenceEvaluator; +} + +FSequencePlayerReference UOLSLocomotionBPLibrary::SetPlayRateToMatchSpeed(const FSequencePlayerReference& sequencePlayer, + float speedToMatch, + FVector2D playRateClamp) +{ + sequencePlayer.CallAnimNodeFunction( + TEXT("SetPlayrateToMatchSpeed"), + [speedToMatch, playRateClamp](FAnimNode_SequencePlayer& sequencePlayer) + { + if (const UAnimSequence* animSequence = Cast(sequencePlayer.GetSequence())) + { + const float animLength = animSequence->GetPlayLength(); + if (!FMath::IsNearlyZero(animLength)) + { + // Calculate the speed as: (distance traveled by the animation) / (length of the animation) + const FVector rootMotionTranslation = animSequence->ExtractRootMotionFromRange(0.0f, animLength). + GetTranslation(); + const float rootMotionDistance = rootMotionTranslation.Size2D(); + if (!FMath::IsNearlyZero(rootMotionDistance)) + { + const float animationSpeed = rootMotionDistance / animLength; + float desiredPlayRate = speedToMatch / animationSpeed; + if (playRateClamp.X >= 0.0f && playRateClamp.X < playRateClamp.Y) + { + desiredPlayRate = FMath::Clamp(desiredPlayRate, playRateClamp.X, playRateClamp.Y); + } + + if (!sequencePlayer.SetPlayRate(desiredPlayRate)) + { + UE_LOG(LogOLSLocomotionLibrary, Warning, + TEXT( + "Could not set play rate on sequence player, value is not dynamic. Set it as Always Dynamic." + )); + } + } + else + { + UE_LOG(LogOLSLocomotionLibrary, Warning, + TEXT("Unable to adjust playrate for animation with no root motion delta (%s)."), + *GetNameSafe(animSequence)); + } + } + else + { + UE_LOG(LogOLSLocomotionLibrary, Warning, + TEXT("Unable to adjust playrate for zero length animation (%s)."), + *GetNameSafe(animSequence)); + } + } + else + { + UE_LOG(LogOLSLocomotionLibrary, Warning, + TEXT("Sequence player does not have an anim sequence to play.")); + } + }); + + return sequencePlayer; +} + FVector UOLSLocomotionBPLibrary::PredictGroundMovementStopLocation(const FVector& velocity, bool shouldUseSeparateBrakingFriction, float brakingFriction, float groundFriction, diff --git a/Source/OLSAnimation/Public/Libraries/OLSLocomotionBPLibrary.h b/Source/OLSAnimation/Public/Libraries/OLSLocomotionBPLibrary.h index 7184186..27ba564 100644 --- a/Source/OLSAnimation/Public/Libraries/OLSLocomotionBPLibrary.h +++ b/Source/OLSAnimation/Public/Libraries/OLSLocomotionBPLibrary.h @@ -3,6 +3,7 @@ #pragma once #include "CoreMinimal.h" +#include "SequenceEvaluatorLibrary.h" #include "Data/OLSEnumLibrary.h" #include "Kismet/BlueprintFunctionLibrary.h" #include "OLSLocomotionBPLibrary.generated.h" @@ -15,8 +16,53 @@ class OLSANIMATION_API UOLSLocomotionBPLibrary : public UBlueprintFunctionLibrar { GENERATED_BODY() +private: + + static float GetDistanceCurveValueAtTime(const UAnimSequenceBase* animSequence, const float time, const FName& curveName); + static float GetTimeAtDistance(const UAnimSequenceBase* animSequence, const float& distance, FName curveName); + public: + /** + * Adjusts the playback time of an animation sequence to match a specified distance to a target. + * + * This function ensures that the animation sequence progresses or adjusts its time based on the distance to a target. + * It uses a distance curve to determine the appropriate time within the animation sequence that corresponds to the desired distance. + * + * @param updateContext The context for the current animation update, providing necessary time and state information. + * @param sequenceEvaluator A reference to the sequence evaluator, which controls and tracks the animation sequence being played. + * @param distanceToTarget The distance to the target that the animation should match. + * Typically, negative values indicate distance curves storing negative distance. + * @param shouldDistanceMatchStop If true, the distance matching stops once the character reaches the target or passes the threshold. + * @param stopDistanceThreshHold The distance threshold at which distance matching should stop. + * If the evaluated distance curve value exceeds this threshold, distance matching will halt. + * @param animEndTime The end time of the animation sequence. If greater than 0, the sequence will not advance past this time. + * @param curveName The name of the curve within the animation sequence that stores distance information. + * This curve is evaluated to determine the time corresponding to the distance to the target. + * + * @return An updated sequence evaluator reference, reflecting the adjusted or advanced animation time. + */ + UFUNCTION(BlueprintCallable, Category = "OLS|Function Library", meta=(BlueprintThreadSafe)) + static FSequenceEvaluatorReference DistanceMatchToTarget(const FAnimUpdateContext& updateContext, + const FSequenceEvaluatorReference& sequenceEvaluator, + float distanceToTarget, + bool shouldDistanceMatchStop, float stopDistanceThreshHold, + float animEndTime, + FName curveName); + + /** + * Set the play rate of the sequence player so that the speed of the animation matches in-game movement speed. + * While distance matching is commonly used for transition animations, cycle animations (walk, jog, etc) typically just adjust their play rate to match + * the in-game movement speed. + * This function assumes that the animation has a constant speed. + * @param sequencePlayer - The sequence player node to operate on. + * @param speedToMatch - The in-game movement speed to match. This is usually the current speed of the movement component. + * @param playRateClamp - A clamp on how much the animation's play rate can change to match the in-game movement speed. Set to (0,0) for no clamping. + */ + UFUNCTION(BlueprintCallable, Category = "OLS|Function Library", meta=(BlueprintThreadSafe)) + static FSequencePlayerReference SetPlayRateToMatchSpeed(const FSequencePlayerReference& sequencePlayer, + float speedToMatch, FVector2D playRateClamp = FVector2D(0.75f, 1.25f)); + /** * Predict where the character will stop based on its current movement properties and parameters from the movement component. * This uses prediction logic that is heavily tied to the UCharacterMovementComponent. @@ -30,12 +76,12 @@ public: float brakingFrictionFactor, float brakingDecelerationWalking); /** - * Predict where the character will change direction during a pivot based on its current movement properties and parameters from the movement component. - * This uses prediction logic that is heavily tied to the UCharacterMovementComponent. - * Each parameter corresponds to a value from the UCharacterMovementComponent with the same name. - * Because this is a thread safe function, it's recommended to populate these fields via the Property Access system. - * @return The predicted pivot position in local space to the character. The size of this vector will be the distance to the pivot. - */ + * Predict where the character will change direction during a pivot based on its current movement properties and parameters from the movement component. + * This uses prediction logic that is heavily tied to the UCharacterMovementComponent. + * Each parameter corresponds to a value from the UCharacterMovementComponent with the same name. + * Because this is a thread safe function, it's recommended to populate these fields via the Property Access system. + * @return The predicted pivot position in local space to the character. The size of this vector will be the distance to the pivot. + */ UFUNCTION(BlueprintCallable, BlueprintPure, Category = "OLS|Function Library", meta=(BlueprintThreadSafe)) static FVector PredictGroundMovementPivotLocation(const FVector& acceleration, const FVector& velocity, float groundFriction);