Animation Notify to adjust TimeDilation of the mesh - Muhwu/unreal-workflows GitHub Wiki
Description
This description maintains a list per Mesh instance to avoid issues with multiple AnimNotifyStates using the same notify state instance. As per my understanding on 2025-05-19, the AnimNotifyStates are instanced per animation asset. This means that multiple actors using the same state can lead into unexpected results.
Notice that this does not currently reliably handle overlapping time dilation tracks and should not be used as such. Improvement ideas are welcome!
C++
TimeDilation.h
#pragma once
#include "CoreMinimal.h"
#include "Animation/AnimNotifies/AnimNotifyState.h"
#include "TimeDilation.generated.h"
USTRUCT()
struct FTimeDilationNotifyInstanceData
{
GENERATED_BODY()
float StartTime;
float Duration;
float PlayRateBeforeStart;
float ElapsedTime;
};
/**
* Time Dilation anim notify state that takes in a curve and bends time for the animation
* through it relative to the position.
*/
UCLASS(Blueprintable, meta = (DisplayName = "Time Dilation"))
class RSOULS_API UTimeDilation : public UAnimNotifyState
{
GENERATED_BODY()
public:
UPROPERTY()
TMap<USkeletalMeshComponent*, FTimeDilationNotifyInstanceData> NotifyInstanceData;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Default")
UCurveFloat* TimeDilationCurve;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Default")
float CurveMultiplier = 1.f;
protected:
virtual void NotifyBegin(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, float TotalDuration, const FAnimNotifyEventReference& EventReference) override;
virtual void NotifyTick(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, float FrameDeltaTime, const FAnimNotifyEventReference& EventReference) override;
virtual void NotifyEnd(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference) override;
};
TimeDilation.cpp
#include "TimeDilation.h"
void UTimeDilation::NotifyBegin(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, float TotalDuration,
const FAnimNotifyEventReference& EventReference)
{
Super::NotifyBegin(MeshComp, Animation, TotalDuration, EventReference);
FTimeDilationNotifyInstanceData* ExistingInstanceData = NotifyInstanceData.Find(MeshComp);
FTimeDilationNotifyInstanceData InstanceData;
InstanceData.Duration = TotalDuration;
// Technically the non-exist default should be the global play rate - however, it appears it is possible for
// there to exist more than a single notif instance in which case the former play rate would be lost and overwritten
// with the current global rate. As such, we enforce the assumption that the play rate before the start of the notif
// is 1.f at the end of any.
InstanceData.PlayRateBeforeStart = 1.f;
InstanceData.StartTime = MeshComp->GetWorld()->GetTimeSeconds();
InstanceData.ElapsedTime = 0.f;
NotifyInstanceData.Add(MeshComp, InstanceData);
if (!TimeDilationCurve)
UE_LOG(LogTemp, Warning, TEXT("Time Dilation Curve is not set."));
}
void UTimeDilation::NotifyTick(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, float FrameDeltaTime,
const FAnimNotifyEventReference& EventReference)
{
Super::NotifyTick(MeshComp, Animation, FrameDeltaTime, EventReference);
if (!TimeDilationCurve)
return;
FTimeDilationNotifyInstanceData* InstanceData = NotifyInstanceData.Find(MeshComp);
if (InstanceData == nullptr)
return;
InstanceData->ElapsedTime += MeshComp->GetWorld()->GetDeltaSeconds() * MeshComp->GlobalAnimRateScale;
const float TimeRatio = InstanceData->ElapsedTime / InstanceData->Duration;
const float CurveValue = TimeDilationCurve->GetFloatValue(TimeRatio);
const float TimeDilation = CurveValue * FMath::Pow(CurveValue, CurveMultiplier);;
MeshComp->GlobalAnimRateScale = TimeDilation;
}
void UTimeDilation::NotifyEnd(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation,
const FAnimNotifyEventReference& EventReference)
{
Super::NotifyEnd(MeshComp, Animation, EventReference);
if (MeshComp && NotifyInstanceData.Contains(MeshComp))
{
MeshComp->GlobalAnimRateScale = NotifyInstanceData[MeshComp].PlayRateBeforeStart;
NotifyInstanceData.Remove(MeshComp);
}
}