diff --git a/Source/Integration/AnimQueue.h b/Source/Integration/AnimQueue.h index f6d00741..54e980f5 100644 --- a/Source/Integration/AnimQueue.h +++ b/Source/Integration/AnimQueue.h @@ -12,10 +12,11 @@ // // An single animation step. // -// The body consists of a sequence of FlxAnimationField -// records. The body is encoded, to read the -// FlxAnimationField records you need an -// FlxAnimationStepDecoder. +// This struct contains an entire animation step. The +// key-value pairs are stored in an encoded form, which is not +// directly accessible to blueprints. To read these key-value +// pairs, blueprints will need to use UnpackAnimationStep or +// AnimationStepGetXXX. // //////////////////////////////////////////////// @@ -27,9 +28,15 @@ public: UPROPERTY() bool Finished; - UPROPERTY() + // The hash of the animation step, a 63-bit unique identifier. + // + UPROPERTY(VisibleAnywhere, BlueprintReadOnly) int64 Hash; + // The Body contains all the key-value pairs in an encoded form. To + // obtain these in a useful form, you will need to use + // UnpackAnimationStep or AnimationStepGetXXX. + // UPROPERTY() TArray Body; diff --git a/Source/Integration/ScriptedAnimation.cpp b/Source/Integration/ScriptedAnimation.cpp new file mode 100644 index 00000000..233e18e2 --- /dev/null +++ b/Source/Integration/ScriptedAnimation.cpp @@ -0,0 +1,177 @@ +#include "ScriptedAnimation.h" + +//////////////////////////////////////////////////////////// +// +// Routines to calculate the current state of an FlxScriptedAnimation. +// +//////////////////////////////////////////////////////////// + + +double FlxScriptedAnimation::CalculateFade(double Offset, double Fade) +{ + if (Offset < 0.0) + { + return 0.0; + } + if (Fade > 0.001) + { + return FMath::Min(Offset / Fade, 1.0); + } + return 1.0; +} + +double FlxScriptedAnimation::UnclampedElapsedTime(double CurrentTime) const +{ + return CurrentTime - StartTime; +} + +double FlxScriptedAnimation::UnclampedTimeLeft(double CurrentTime) const +{ + return EndTime - CurrentTime; +} + +double FlxScriptedAnimation::ClampedElapsedTime(double CurrentTime) const +{ + return FMath::Max(0.0, CurrentTime - StartTime); +} + +double FlxScriptedAnimation::ClampedTimeLeft(double CurrentTime) const +{ + return FMath::Max(0.0, EndTime - CurrentTime); +} + +double FlxScriptedAnimation::CalcFadeInAlpha(double CurrentTime) const +{ + return CalculateFade(UnclampedElapsedTime(CurrentTime), FadeInDuration); +} + +double FlxScriptedAnimation::CalcFadeOutAlpha(double CurrentTime) const +{ + return CalculateFade(UnclampedTimeLeft(CurrentTime), FadeOutDuration); +} + +double FlxScriptedAnimation::CalcFadeInOutAlpha(double CurrentTime) const +{ + return FMath::Min(CalcFadeInAlpha(CurrentTime), CalcFadeOutAlpha(CurrentTime)); +} + +void FlxScriptedAnimation::InitiateFadeOut(double CurrentTime, double AllowedFade) +{ + // We only need to truncate if we aren't already fading out, + // or if the in-progress fadeout isn't sufficiently fast. + // + double FadeOutAlpha = CalcFadeOutAlpha(CurrentTime); + if ((FadeOutAlpha >= 1.0) || (FadeOutDuration > AllowedFade)) + { + double CurrentAlpha = FMath::Min(CalcFadeInAlpha(CurrentTime), FadeOutAlpha); + double NewFadeOutDuration = FMath::Min(AllowedFade, FadeOutDuration); + double NewTimeLeft = NewFadeOutDuration * CurrentAlpha; + EndTime = CurrentTime + NewTimeLeft; + FadeOutDuration = NewFadeOutDuration; + } +} + +void UlxScriptedAnimations::Keep(int n) +{ + if (n < 0) n = 0; + if (Animations.Num() > n) + { + Animations.SetNum(n); + } +} + +void UlxScriptedAnimations::AddAnimation( + UObject* WorldContextObject, UAnimSequenceBase* Sequence, double FadeInTime, double FadeOutTime, int64 AqHash) +{ + check(KeepCount >= 1); + FlxScriptedAnimation Result; + + // Get World Time + UWorld* World = GEngine->GetWorldFromContextObjectChecked(WorldContextObject); + double WorldTime = World ? World->GetTimeSeconds() : 0.0; + + // Get the animation Length. + double Length = (Sequence ? static_cast(Sequence->GetPlayLength()) : 0.0); + + // Fill the static setup fields + Result.Sequence = Sequence; + Result.AqHash = AqHash; + Result.FadeInDuration = FadeInTime; + Result.FadeOutDuration = FadeOutTime; + Result.StartTime = WorldTime; + Result.EndTime = WorldTime + Length; + + Keep(KeepCount - 1); + Animations.Insert(Result, 0); +} + +void UlxScriptedAnimations::FadeGarbage(TArray Hashes, double CurrentTime) +{ + for (int i = 0; i < Animations.Num(); i++) + { + FlxScriptedAnimation &Anim = Animations[i]; + if ((Anim.AqHash != 0) && (!Hashes.Contains(Anim.AqHash))) + { + Anim.InitiateFadeOut(CurrentTime, 0.2); + } + } +} + +void UlxScriptedAnimationLibrary::ScriptedAnimationEvaluatorData(const UlxScriptedAnimations *Animations, double CurrentTime, + UAnimSequenceBase *&Sequence0, float &ExplicitTime0, + UAnimSequenceBase *&Sequence1, float &ExplicitTime1, + UAnimSequenceBase *&Sequence2, float &ExplicitTime2, + float &BaseAlpha, float &Sequence0Alpha, float &Sequence1Alpha, float &Sequence2Alpha) +{ + Sequence0 = nullptr; + Sequence1 = nullptr; + Sequence2 = nullptr; + ExplicitTime0 = 0.0; + ExplicitTime1 = 0.0; + ExplicitTime2 = 0.0; + BaseAlpha = 0.0; + Sequence0Alpha = 0.0; + Sequence1Alpha = 0.0; + Sequence2Alpha = 0.0; + + if (Animations != nullptr) + { + const TArray &Anims = Animations->GetAnimations(); + if (Anims.Num() > 0) + { + const FlxScriptedAnimation &Anim = Anims[0]; + Sequence0 = Anim.Sequence; + ExplicitTime0 = Anim.ClampedElapsedTime(CurrentTime); + Sequence0Alpha = Anim.CalcFadeInOutAlpha(CurrentTime); + } + if (Anims.Num() > 1) + { + const FlxScriptedAnimation &Anim = Anims[1]; + Sequence1 = Anim.Sequence; + ExplicitTime1 = Anim.ClampedElapsedTime(CurrentTime); + Sequence1Alpha = Anim.CalcFadeInOutAlpha(CurrentTime); + } + if (Anims.Num() > 2) + { + const FlxScriptedAnimation &Anim = Anims[2]; + Sequence2 = Anim.Sequence; + ExplicitTime2 = Anim.ClampedElapsedTime(CurrentTime); + Sequence2Alpha = Anim.CalcFadeInOutAlpha(CurrentTime); + } + } + + double AlphaTotal = Sequence0Alpha + Sequence1Alpha + Sequence2Alpha; + if (AlphaTotal > 1.0) + { + double Scale = 1.0 / AlphaTotal; + Sequence0Alpha *= Scale; + Sequence1Alpha *= Scale; + Sequence2Alpha *= Scale; + BaseAlpha = 0.0; + } + else + { + BaseAlpha = 1.0 - AlphaTotal; + } +} + diff --git a/Source/Integration/ScriptedAnimation.h b/Source/Integration/ScriptedAnimation.h new file mode 100644 index 00000000..856f4c3b --- /dev/null +++ b/Source/Integration/ScriptedAnimation.h @@ -0,0 +1,174 @@ +#include "CoreMinimal.h" +// #include "Kismet/KismetSystemLibrary.h" +// #include "CommonTypes.h" +// #include "Kismet/BlueprintFunctionLibrary.h" + +#include "ScriptedAnimation.generated.h" + +class UEnhancedInputLocalPlayerSubsystem; +class UAnimSequenceBase; + + +USTRUCT(BlueprintType) +struct INTEGRATION_API FlxScriptedAnimation +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite) + UAnimSequenceBase *Sequence = nullptr; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, AdvancedDisplay) + int64 AqHash = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, AdvancedDisplay) + double FadeInDuration = 0.0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, AdvancedDisplay) + double FadeOutDuration = 0.0; + + // StartTime and EndTime. + // + // StartTime is the world time when the animation was actually started. + // That is immutable once the animation is created. However, EndTime is + // mutable: initially, it is set to StartTime + Animation.Length. But + // script requests that the animation be truncated, the EndTime + // may be reduced to implement the truncation. + // + + UPROPERTY(EditAnywhere, BlueprintReadWrite, AdvancedDisplay) + double StartTime = 0.0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, AdvancedDisplay) + double EndTime = 0.0; + + // Calculate the progress of a fade. + // + static double CalculateFade(double Offset, double Fade); + + // Calculate Elapsed Time (unclamped) + // + // If current time is before the animation's start time, then + // elapsed time will be negative. + // + double UnclampedElapsedTime(double CurrentTime) const; + + // Calculate Time Left (unclamped) + // + // If current time is after the animation's end time, then + // time left will be negative. + // + double UnclampedTimeLeft(double CurrentTime) const; + + // Calculate Elapsed Time (clamped) + // + // If current time is before the animation's start time, then + // elapsed time will be zero. + // + double ClampedElapsedTime(double CurrentTime) const; + + // Calculate Time Left (clamped) + // + // If current time is after the animation's end time, then + // time left will be zero. + // + double ClampedTimeLeft(double CurrentTime) const; + + // Calculate the progress of the fadein. + // + double CalcFadeInAlpha(double CurrentTime) const; + + // Calculate the progress of the fadeout. + // + double CalcFadeOutAlpha(double CurrentTime) const; + + // Calculate the combined alpha of the fade-in-out. + // + double CalcFadeInOutAlpha(double CurrentTime) const; + + // Cause an animation to start fading right away. + // + // Sets up the animation to start fading immediately, by adjusting + // the animation's EndTime. If the FadeOut time of the animation + // is longer than the allowed fade, that will be reduced. + // If the animation was already in the process of fading in + // or fading out, then that will be taken into account. + // + // If you want to chop an animation off abruptly, without any fade, + // simply set AllowedFade to 0.0. + // + void InitiateFadeOut(double CurrentTime, double AllowedFade); +}; + + +UCLASS(BlueprintType) +class INTEGRATION_API UlxScriptedAnimations : public UObject +{ + GENERATED_BODY() + +private: + UPROPERTY() + int KeepCount = 3; + + UPROPERTY() + TArray Animations; + + // If the number of elements in the array exceeds the count, discard. + // + void Keep(int n); + +public: + // Allow read-only access anywhere. + // + const TArray &GetAnimations() const { return Animations; } + + // Add a scripted animation. + // + // The step is inserted at the front of the array: the first animation + // in the array is always the most recent. All other steps are shifted + // back. If the array size exceeds the KeepCount, it is truncated. + // + // The Aq Hash parameter can be used to associate a scripted animation + // with a step in the animation queue. This makes it feasible to garbage + // collect the scripted animation if the animation queue step disappears. + // + UFUNCTION(BlueprintCallable, Category = "Luprex|Scripted Animations", meta=(WorldContext = "WorldContextObject")) + void AddAnimation( + UObject *WorldContextObject, UAnimSequenceBase* Sequence, double FadeInTime = 0.2, double FadeOutTime = 0.2, int64 AqHash=0); + + // Fade animations whose corresponding animation queue step is dead. + // + // Sometimes, predictive reexecution causes an animation step to + // vanish from the animation queue. When that happens, it is usually + // desirable to terminate any scripted animations that were launched + // by that animation step. You don't want to cut them off abruptly, + // you want to fade them out. This function causes all animations that + // are owned by a dead animation step to begin fading. + // + UFUNCTION(BlueprintCallable, Category = "Luprex|Scripted Animations", meta=(WorldContext = "WorldContextObject")) + void FadeGarbage(TArray Hashes, double CurrentTime); +}; + +UCLASS() +class INTEGRATION_API UlxScriptedAnimationLibrary : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +private: + + +public: + // Get the data to drive Sequence Evaluators and Multi Blend + // + // To apply scripted animations in an Anim Graph, you will need + // three sequence evaluators and a multi-blend node. This + // function outputs the input parameters for all of those nodes. + // + UFUNCTION(BlueprintPure, Category = "Luprex|Scripted Animations", meta=(BlueprintThreadSafe)) + static void ScriptedAnimationEvaluatorData(const UlxScriptedAnimations *Animations, double CurrentTime, + UAnimSequenceBase *&Sequence0, float &ExplicitTime0, + UAnimSequenceBase *&Sequence1, float &ExplicitTime1, + UAnimSequenceBase *&Sequence2, float &ExplicitTime2, + float &BaseAlpha, float &Sequence0Alpha, float &Sequence1Alpha, float &Sequence2Alpha); + +}; + diff --git a/Source/Integration/UtilityLibrary.cpp b/Source/Integration/UtilityLibrary.cpp index 08f987e2..01ba37cd 100644 --- a/Source/Integration/UtilityLibrary.cpp +++ b/Source/Integration/UtilityLibrary.cpp @@ -213,121 +213,6 @@ void UlxUtilityLibrary::GetPositionOfGridPanelMiddleCell(UGridPanel *GridPanel, } } -void UlxScriptedAnimations::Keep(int n) -{ - if (n < 0) n = 0; - if (Animations.Num() > n) - { - Animations.SetNum(n); - } -} - -void UlxScriptedAnimations::AddAnimation( - UObject* WorldContextObject, UAnimSequenceBase* Sequence, double FadeInTime, double FadeOutTime) -{ - check(KeepCount >= 1); - FlxScriptedAnimation Result; - - // Get World Time - UWorld* World = GEngine->GetWorldFromContextObjectChecked(WorldContextObject); - double WorldTime = World ? World->GetTimeSeconds() : 0.0; - - // Fill the static setup fields - Result.Sequence = Sequence; - Result.FadeIn = FadeInTime; - Result.FadeOut = FadeOutTime; - Result.StartTime = WorldTime; - Result.AdjustedLength = (Result.Sequence ? static_cast(Result.Sequence->GetPlayLength()) : 0.0); - - Keep(KeepCount - 1); - Animations.Insert(Result, 0); -} - -void UlxScriptedAnimations::FadeOutAll(UObject *WorldContextObject) -{ - -} - -FlxScriptedAnimationProgress UlxUtilityLibrary::ScriptedAnimationProgress(const FlxScriptedAnimation &Animation, double CurrentTime) -{ - FlxScriptedAnimationProgress Progress(Animation); - - // Store the world time of the last update. - Progress.UpdateTime = CurrentTime; - - // Compute time relationships - Progress.EndTime = Animation.StartTime + Progress.AdjustedLength; - Progress.ElapsedTime = FMath::Max(0.0, Progress.UpdateTime - Progress.StartTime); - Progress.TimeLeft = FMath::Max(0.0, Progress.EndTime - Progress.UpdateTime); - - // Determine fade-in / fade-out blend - Progress.FadeInAlpha = 1.0; - Progress.FadeOutAlpha = 1.0; - if (Progress.FadeIn > 0.0) Progress.FadeInAlpha = FMath::Clamp(Progress.ElapsedTime / Progress.FadeIn, 0.0, 1.0); - if (Progress.FadeOut > 0.0) Progress.FadeOutAlpha = FMath::Clamp(Progress.TimeLeft / Progress.FadeOut, 0.0, 1.0); - Progress.FadeAlpha = FMath::Min(Progress.FadeInAlpha, Progress.FadeOutAlpha); - - return Progress; -} - -void UlxUtilityLibrary::ScriptedAnimationEvaluatorData(const UlxScriptedAnimations *Animations, double CurrentTime, - UAnimSequenceBase *&Sequence0, float &ExplicitTime0, - UAnimSequenceBase *&Sequence1, float &ExplicitTime1, - UAnimSequenceBase *&Sequence2, float &ExplicitTime2, - float &BaseAlpha, float &Sequence0Alpha, float &Sequence1Alpha, float &Sequence2Alpha) -{ - Sequence0 = nullptr; - Sequence1 = nullptr; - Sequence2 = nullptr; - ExplicitTime0 = 0.0; - ExplicitTime1 = 0.0; - ExplicitTime2 = 0.0; - BaseAlpha = 0.0; - Sequence0Alpha = 0.0; - Sequence1Alpha = 0.0; - Sequence2Alpha = 0.0; - - if (Animations != nullptr) - { - const TArray &Anims = Animations->GetAnimations(); - if (Anims.Num() > 0) - { - FlxScriptedAnimationProgress Progress = ScriptedAnimationProgress(Anims[0], CurrentTime); - Sequence0 = Progress.Sequence; - ExplicitTime0 = Progress.ElapsedTime; - Sequence0Alpha = Progress.FadeAlpha; - } - if (Anims.Num() > 1) - { - FlxScriptedAnimationProgress Progress = ScriptedAnimationProgress(Anims[1], CurrentTime); - Sequence1 = Progress.Sequence; - ExplicitTime1 = Progress.ElapsedTime; - Sequence1Alpha = Progress.FadeAlpha; - } - if (Anims.Num() > 2) - { - FlxScriptedAnimationProgress Progress = ScriptedAnimationProgress(Anims[2], CurrentTime); - Sequence2 = Progress.Sequence; - ExplicitTime2 = Progress.ElapsedTime; - Sequence2Alpha = Progress.FadeAlpha; - } - } - - double AlphaTotal = Sequence0Alpha + Sequence1Alpha + Sequence2Alpha; - if (AlphaTotal > 1.0) - { - double Scale = 1.0 / AlphaTotal; - Sequence0Alpha *= Scale; - Sequence1Alpha *= Scale; - Sequence2Alpha *= Scale; - BaseAlpha = 0.0; - } - else - { - BaseAlpha = 1.0 - AlphaTotal; - } -} - ElxUsedOrNotUsed UlxUtilityLibrary::IsKeyUsedByMappingContext(const FKey &Key, const UInputMappingContext *MappingContext) { if (!MappingContext) diff --git a/Source/Integration/UtilityLibrary.h b/Source/Integration/UtilityLibrary.h index 07fa9e79..34644da3 100644 --- a/Source/Integration/UtilityLibrary.h +++ b/Source/Integration/UtilityLibrary.h @@ -14,89 +14,6 @@ class UEnhancedInputLocalPlayerSubsystem; class UAnimSequenceBase; -USTRUCT(BlueprintType) -struct INTEGRATION_API FlxScriptedAnimation -{ - GENERATED_BODY() - - UPROPERTY(EditAnywhere, BlueprintReadWrite) - UAnimSequenceBase *Sequence; - - UPROPERTY(EditAnywhere, BlueprintReadWrite, AdvancedDisplay) - double FadeIn = 0.0; - - UPROPERTY(EditAnywhere, BlueprintReadWrite, AdvancedDisplay) - double FadeOut = 0.0; - - UPROPERTY(EditAnywhere, BlueprintReadWrite, AdvancedDisplay) - double StartTime = 0.0; - - UPROPERTY(EditAnywhere, BlueprintReadWrite, AdvancedDisplay) - double AdjustedLength = 0.0; -}; - -USTRUCT(BlueprintType) -struct INTEGRATION_API FlxScriptedAnimationProgress : public FlxScriptedAnimation -{ - GENERATED_BODY() - - UPROPERTY(EditAnywhere, BlueprintReadWrite, AdvancedDisplay) - double UpdateTime = 0.0; - - UPROPERTY(EditAnywhere, BlueprintReadWrite, AdvancedDisplay) - double EndTime = 0.0; - - UPROPERTY(EditAnywhere, BlueprintReadWrite, AdvancedDisplay) - double ElapsedTime = 0.0; - - UPROPERTY(EditAnywhere, BlueprintReadWrite, AdvancedDisplay) - double TimeLeft = 0.0; - - UPROPERTY(EditAnywhere, BlueprintReadWrite, AdvancedDisplay) - double FadeInAlpha = 0.0; - - UPROPERTY(EditAnywhere, BlueprintReadWrite, AdvancedDisplay) - double FadeOutAlpha = 0.0; - - UPROPERTY(EditAnywhere, BlueprintReadWrite, AdvancedDisplay) - double FadeAlpha = 0.0; -}; - - -UCLASS(BlueprintType) -class INTEGRATION_API UlxScriptedAnimations : public UObject -{ - GENERATED_BODY() - -private: - UPROPERTY() - int KeepCount = 3; - - UPROPERTY() - TArray Animations; - - // If the number of elements in the array exceeds the count, discard. - // - void Keep(int n); - -public: - // Allow read-only access anywhere. - // - const TArray &GetAnimations() const { return Animations; } - - // Add a scripted animation to the end of the array. - // - UFUNCTION(BlueprintCallable, Category = "Luprex|Scripted Animations", meta=(WorldContext = "WorldContextObject")) - void AddAnimation( - UObject *WorldContextObject, UAnimSequenceBase* Sequence, double FadeInTime = 0.2, double FadeOutTime = 0.2); - - // Truncate all current animations: force them all to begin fading out. - // - UFUNCTION(BlueprintCallable, Category = "Luprex|Scripted Animations", meta=(WorldContext = "WorldContextObject")) - void FadeOutAll(UObject *WorldContextObject); -}; - - /** * * UlxUtilityLibrary is for functions that are aren't particularly luprex-specific, @@ -220,29 +137,6 @@ public: UFUNCTION(BlueprintPure, Category="Widget") static void GetPositionOfGridPanelMiddleCell(UGridPanel *GridPanel, FVector2D &UpperLeftXY, FVector2D &LowerRightXY); - // Calculate the progress of a Scripted Animation. - // - // Given a scripted animation and the current time, calculates - // how much time has elapsed, how much time is left, and several - // other parameters pertaining to the passage of time for - // this animation. - // - UFUNCTION(BlueprintPure, Category = "Luprex|Scripted Animations", meta=(BlueprintThreadSafe)) - static FlxScriptedAnimationProgress ScriptedAnimationProgress(const FlxScriptedAnimation &Animation, double CurrentTime); - - // Get the data to drive Sequence Evaluators and Multi Blend - // - // To apply scripted animations in an Anim Graph, you will need - // three sequence evaluators and a multi-blend node. This - // function outputs the input parameters for all of those nodes. - // - UFUNCTION(BlueprintPure, Category = "Luprex|Scripted Animations", meta=(BlueprintThreadSafe)) - static void ScriptedAnimationEvaluatorData(const UlxScriptedAnimations *Animations, double CurrentTime, - UAnimSequenceBase *&Sequence0, float &ExplicitTime0, - UAnimSequenceBase *&Sequence1, float &ExplicitTime1, - UAnimSequenceBase *&Sequence2, float &ExplicitTime2, - float &BaseAlpha, float &Sequence0Alpha, float &Sequence1Alpha, float &Sequence2Alpha); - // Check if a given key is used by the specified mapping context. // // This is true if the key is mapped to anything at all within