333 lines
10 KiB
C++
333 lines
10 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "Animation/UMGSequenceTickManager.h"
|
|
#include "Animation/UMGSequencePlayer.h"
|
|
#include "Blueprint/UserWidget.h"
|
|
#include "Engine/World.h"
|
|
#include "EntitySystem/MovieSceneEntitySystemRunner.h"
|
|
#include "EntitySystem/MovieSceneEntitySystemLinker.h"
|
|
#include "ProfilingDebugging/CountersTrace.h"
|
|
#include "Framework/Application/SlateApplication.h"
|
|
#include "Engine/Engine.h"
|
|
|
|
#include UE_INLINE_GENERATED_CPP_BY_NAME(UMGSequenceTickManager)
|
|
|
|
DECLARE_CYCLE_STAT(TEXT("Flush End of Frame Animations"), MovieSceneEval_FlushEndOfFrameAnimations, STATGROUP_MovieSceneEval);
|
|
|
|
|
|
namespace UE::UMG
|
|
{
|
|
|
|
static TAutoConsoleVariable<int32> CVarUMGMaxAnimationLatentActions(
|
|
TEXT("Widget.MaxAnimationLatentActions"),
|
|
100,
|
|
TEXT("Defines the maximum number of latent actions that can be run in one frame."),
|
|
ECVF_Default
|
|
);
|
|
int32 GFlushUMGAnimationsAtEndOfFrame = 1;
|
|
static FAutoConsoleVariableRef CVarUMGAnimationsAtEndOfFrame(
|
|
TEXT("UMG.FlushAnimationsAtEndOfFrame"),
|
|
GFlushUMGAnimationsAtEndOfFrame,
|
|
TEXT("Whether to automatically flush any outstanding animations at the end of the frame, or just wait until next frame."),
|
|
ECVF_Default
|
|
);
|
|
|
|
float GAnimationBudgetMs = 0.0f;
|
|
FAutoConsoleVariableRef CVarAnimationBudgetMs(
|
|
TEXT("UMG.AnimationBudgetMs"),
|
|
GAnimationBudgetMs,
|
|
TEXT("(Default: 0.0) EXPERIMENTAL: A per-frame animation budget to use for evaluation of all UMG animations this frame.")
|
|
);
|
|
} // namespace UE::UMG
|
|
|
|
UUMGSequenceTickManager::UUMGSequenceTickManager(const FObjectInitializer& Init)
|
|
: Super(Init)
|
|
, bIsTicking(false)
|
|
{
|
|
}
|
|
|
|
void UUMGSequenceTickManager::Initialize(UObject* Owner)
|
|
{
|
|
Linker = UMovieSceneEntitySystemLinker::FindOrCreateLinker(Owner, UE::MovieScene::EEntitySystemLinkerRole::UMG, TEXT("UMGAnimationEntitySystemLinker"));
|
|
check(Linker);
|
|
Runner = Linker->GetRunner();
|
|
|
|
FSlateApplication& SlateApp = FSlateApplication::Get();
|
|
FDelegateHandle PreTickHandle = SlateApp.OnPreTick().AddUObject(this, &UUMGSequenceTickManager::TickWidgetAnimations);
|
|
check(PreTickHandle.IsValid());
|
|
SlateApplicationPreTickHandle = PreTickHandle;
|
|
|
|
FDelegateHandle PostTickHandle = SlateApp.OnPostTick().AddUObject(this, &UUMGSequenceTickManager::HandleSlatePostTick);
|
|
check(PostTickHandle.IsValid());
|
|
SlateApplicationPostTickHandle = PostTickHandle;
|
|
}
|
|
|
|
void UUMGSequenceTickManager::AddWidget(UUserWidget* InWidget)
|
|
{
|
|
// This is functionally the same as OnWidgetTicked, but they remain
|
|
// separate functions to convey the semantic difference
|
|
TWeakObjectPtr<UUserWidget> WeakWidget = InWidget;
|
|
|
|
if (FSequenceTickManagerWidgetData* WidgetData = WeakUserWidgetData.Find(WeakWidget))
|
|
{
|
|
WidgetData->bIsTicking = true;
|
|
}
|
|
else
|
|
{
|
|
WeakUserWidgetData.Add(WeakWidget, FSequenceTickManagerWidgetData());
|
|
}
|
|
}
|
|
|
|
void UUMGSequenceTickManager::RemoveWidget(UUserWidget* InWidget)
|
|
{
|
|
ClearLatentActions(InWidget);
|
|
TWeakObjectPtr<UUserWidget> WeakWidget = InWidget;
|
|
WeakUserWidgetData.Remove(WeakWidget);
|
|
}
|
|
|
|
void UUMGSequenceTickManager::OnWidgetTicked(UUserWidget* InWidget)
|
|
{
|
|
if (FSequenceTickManagerWidgetData* WidgetData = WeakUserWidgetData.Find(InWidget))
|
|
{
|
|
WidgetData->bIsTicking = true;
|
|
}
|
|
else
|
|
{
|
|
WeakUserWidgetData.Add(InWidget, FSequenceTickManagerWidgetData());
|
|
}
|
|
}
|
|
|
|
void UUMGSequenceTickManager::BeginDestroy()
|
|
{
|
|
if (SlateApplicationPreTickHandle.IsValid())
|
|
{
|
|
if (FSlateApplication::IsInitialized())
|
|
{
|
|
FSlateApplication& SlateApp = FSlateApplication::Get();
|
|
SlateApp.OnPreTick().Remove(SlateApplicationPreTickHandle);
|
|
SlateApplicationPreTickHandle.Reset();
|
|
|
|
SlateApp.OnPostTick().Remove(SlateApplicationPostTickHandle);
|
|
SlateApplicationPostTickHandle.Reset();
|
|
}
|
|
}
|
|
|
|
Super::BeginDestroy();
|
|
}
|
|
|
|
void UUMGSequenceTickManager::TickWidgetAnimations(float DeltaSeconds)
|
|
{
|
|
if (bIsTicking)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (IsUnreachable() || HasAnyFlags(RF_BeginDestroyed) || Linker == nullptr || Linker->IsUnreachable() || Linker->HasAnyFlags(RF_BeginDestroyed))
|
|
{
|
|
// Speculatively ignore any kinds of updates if any of the required objects are in the process of being torn down
|
|
return;
|
|
}
|
|
|
|
// Don't tick the animation if inside of a PostLoad
|
|
if (FUObjectThreadContext::Get().IsRoutingPostLoad)
|
|
{
|
|
return;
|
|
}
|
|
|
|
TGuardValue<bool> IsTickingGuard(bIsTicking, true);
|
|
|
|
// Tick all animations in all active widgets.
|
|
//
|
|
// In the main code path (the one where animations are just chugging along), the UMG sequence players
|
|
// will queue evaluations on the global sequencer ECS linker. In some specific cases, though (pausing,
|
|
// stopping, etc.), we might see some blocking (immediate) evaluations running here.
|
|
//
|
|
// The WidgetData have one frame delay (they are updated at the end of the frame).
|
|
// This may delay the animation update by one frame.
|
|
const bool bIsCurrentlyEvaluating = Runner->IsCurrentlyEvaluating();
|
|
{
|
|
SCOPE_CYCLE_UOBJECT(ContextScope, this);
|
|
|
|
// Process animations for visible widgets
|
|
for (auto WidgetIter = WeakUserWidgetData.CreateIterator(); WidgetIter; ++WidgetIter)
|
|
{
|
|
UUserWidget* UserWidget = WidgetIter.Key().Get();
|
|
FSequenceTickManagerWidgetData& WidgetData = WidgetIter.Value();
|
|
|
|
WidgetData.bActionsAndAnimationTicked = false;
|
|
|
|
if (!UserWidget)
|
|
{
|
|
WidgetIter.RemoveCurrent();
|
|
}
|
|
else if (!UserWidget->IsConstructed())
|
|
{
|
|
if (!bIsCurrentlyEvaluating)
|
|
{
|
|
// Tear down any animations that are not currently being stopped
|
|
UserWidget->ConditionalTearDownAnimations();
|
|
UserWidget->UpdateCanTick();
|
|
|
|
// If there are no more animations playing, we can remove this widget altogether
|
|
if (!UserWidget->IsAnyAnimationPlaying())
|
|
{
|
|
// Resetting the animation tick manager is ok here because TearDownAnimations will always clear out all animations
|
|
UserWidget->AnimationTickManager = nullptr;
|
|
|
|
WidgetIter.RemoveCurrent();
|
|
}
|
|
}
|
|
}
|
|
else if (!WidgetData.bIsTicking)
|
|
{
|
|
// If this widget has not told us it is ticking, we disable animations for that widget.
|
|
// Once it ticks again, the animation will be updated naturally, and doesn't need anything re-enabling.
|
|
//
|
|
// @todo: There is a chance that relative animations hitting this code path will resume with
|
|
// different relative bases due to the way the ecs data is destroyed and re-created.
|
|
// In order to fix this we would have to annex that data instead of destroying it.
|
|
if (!bIsCurrentlyEvaluating)
|
|
{
|
|
UserWidget->DisableAnimations();
|
|
|
|
// Do not null out UUserWidget::AnimationTickManager because although we removed animation _data_
|
|
// the animations themselves are still playing. As such any UUMGSequencePlayers may hold a reference to this
|
|
// tick manager's linker, and therefore also need to keep this tick manager alive since the linker is not outered to this tick manager
|
|
|
|
WidgetIter.RemoveCurrent();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
SCOPE_CYCLE_UOBJECT(WidgetContextScope, UserWidget);
|
|
|
|
#if WITH_EDITOR
|
|
const bool bTickAnimations = !UserWidget->IsDesignTime();
|
|
#else
|
|
const bool bTickAnimations = true;
|
|
#endif
|
|
if (bTickAnimations && UserWidget->IsVisible())
|
|
{
|
|
UserWidget->TickActionsAndAnimation(DeltaSeconds);
|
|
WidgetData.bActionsAndAnimationTicked = true;
|
|
}
|
|
|
|
// Assume this widget will no longer tick, until we're told otherwise by way of OnWidgetTicked
|
|
WidgetData.bIsTicking = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
ForceFlush();
|
|
|
|
if (!Runner->IsCurrentlyEvaluating())
|
|
{
|
|
for (auto WidgetIter = WeakUserWidgetData.CreateIterator(); WidgetIter; ++WidgetIter)
|
|
{
|
|
UUserWidget* UserWidget = WidgetIter.Key().Get();
|
|
ensureMsgf(UserWidget, TEXT("Widget became null during animation tick!"));
|
|
|
|
if (UserWidget)
|
|
{
|
|
// If this widget no longer has any animations playing, it doesn't need to be ticked any more
|
|
if (!UserWidget->IsAnyAnimationPlaying())
|
|
{
|
|
UserWidget->UpdateCanTick();
|
|
UserWidget->AnimationTickManager = nullptr;
|
|
WidgetIter.RemoveCurrent();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
WidgetIter.RemoveCurrent();
|
|
}
|
|
}
|
|
}
|
|
|
|
WeakUserWidgetData.Shrink();
|
|
}
|
|
|
|
void UUMGSequenceTickManager::ForceFlush()
|
|
{
|
|
Runner->Flush(UE::UMG::GAnimationBudgetMs);
|
|
RunLatentActions();
|
|
}
|
|
|
|
void UUMGSequenceTickManager::HandleSlatePostTick(float DeltaSeconds)
|
|
{
|
|
// Early out if inside a PostLoad
|
|
if (FUObjectThreadContext::Get().IsRoutingPostLoad)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Only tick widgets at the end of the frame if our runner has completely finished, and we still have updates
|
|
if (UE::UMG::GFlushUMGAnimationsAtEndOfFrame && Runner->HasQueuedUpdates() && !Runner->IsCurrentlyEvaluating())
|
|
{
|
|
SCOPE_CYCLE_COUNTER(MovieSceneEval_FlushEndOfFrameAnimations);
|
|
|
|
Runner->Flush();
|
|
RunLatentActions();
|
|
}
|
|
}
|
|
|
|
void UUMGSequenceTickManager::AddLatentAction(FMovieSceneSequenceLatentActionDelegate Delegate)
|
|
{
|
|
LatentActionManager.AddLatentAction(Delegate);
|
|
}
|
|
|
|
void UUMGSequenceTickManager::ClearLatentActions(UObject* Object)
|
|
{
|
|
LatentActionManager.ClearLatentActions(Object);
|
|
}
|
|
|
|
void UUMGSequenceTickManager::RunLatentActions()
|
|
{
|
|
if (!this->Runner->IsCurrentlyEvaluating())
|
|
{
|
|
int32 UpdateCount = this->Runner->GetQueuedUpdateCount();
|
|
uint64 SystemSerial = this->Linker->EntityManager.GetSystemSerial();
|
|
|
|
LatentActionManager.RunLatentActions([this, &SystemSerial, &UpdateCount]
|
|
{
|
|
int32 NewUpdateCount = this->Runner->GetQueuedUpdateCount();
|
|
uint64 NewSystemSerial = this->Linker->EntityManager.GetSystemSerial();
|
|
if (NewUpdateCount != UpdateCount || NewSystemSerial != SystemSerial)
|
|
{
|
|
UpdateCount = NewUpdateCount;
|
|
SystemSerial = NewSystemSerial;
|
|
|
|
this->Runner->Flush();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
UUMGSequenceTickManager* UUMGSequenceTickManager::Get(UObject* PlaybackContext)
|
|
{
|
|
const TCHAR* TickManagerName = TEXT("GlobalUMGSequenceTickManager");
|
|
|
|
// The tick manager is owned by GEngine to ensure that it is kept alive for widgets that do not belong to
|
|
// a world, but still require animations to be ticked. Ultimately this class could become an engine subsystem
|
|
// but that would mean it is still around and active even if there are no animations playing, which is less
|
|
// than ideal
|
|
UObject* Owner = GEngine;
|
|
if (!ensure(Owner))
|
|
{
|
|
// If (in the hopefully impossible event) there is no engine, use the previous method of a World as a fallback.
|
|
// This will at least ensure we do not crash at the callsite due to a null tick manager
|
|
check(PlaybackContext != nullptr && PlaybackContext->GetWorld() != nullptr);
|
|
Owner = PlaybackContext->GetWorld();
|
|
}
|
|
|
|
UUMGSequenceTickManager* TickManager = FindObject<UUMGSequenceTickManager>(Owner, TickManagerName);
|
|
if (!TickManager)
|
|
{
|
|
TickManager = NewObject<UUMGSequenceTickManager>(Owner, TickManagerName);
|
|
TickManager->Initialize(Owner);
|
|
}
|
|
return TickManager;
|
|
}
|
|
|
|
|