UMG LATENT ACTION: MOVE WIDGET
For this blueprint function, you are going to need the source code.
First find the Engine\Source\Runtime\Engine\Private folder, and add the following .h file. I called it InterpolateWidgetToAction.h (since the move component action code shares the same naming convention):
// Copyright 1998-2015 Epic Games, Inc. All Rights Reserved.
#pragma once
/** Action that interpolates a component over time to a desired position */
class FInterpolateWidgetToAction : public FPendingLatentAction
{
public:
/** Time over which interpolation should happen */
float TotalTime;
/** Time so far elapsed for the interpolation */
float TimeElapsed;
/** If we are currently interpolating. If false, update will complete */
bool bInterpolating;
/** Function to execute on completion */
FName ExecutionFunction;
/** Link to fire on completion */
int32 OutputLink;
/** Object to call callback on upon completion */
FWeakObjectPtr CallbackTarget;
/** Component to interpolate */
TWeakObjectPtr<UWidget> TargetWidget;
/** If we should modify location */
bool bInterpTranslation;
/** Location to interpolate from */
FVector2D InitialTranslation;
/** Location to interpolate to */
FVector2D TargetTranslation;
/** If we should modify location */
bool bInterpScale;
/** Location to interpolate from */
FVector2D InitialScale;
/** Location to interpolate to */
FVector2D TargetScale;
/** If we should modify location */
bool bInterpShear;
/** Location to interpolate from */
FVector2D InitialShear;
/** Location to interpolate to */
FVector2D TargetShear;
/** If we should modify rotation */
bool bInterpAngle;
/** Rotation to interpolate from */
float InitialAngle;
/** Rotation to interpolate to */
float TargetAngle;
/** Should we ease in (ie start slowly) during interpolation */
bool bEaseIn;
/** Should we east out (ie end slowly) during interpolation */
bool bEaseOut;
FInterpolateWidgetToAction(float Duration, const FLatentActionInfo& LatentInfo, UWidget* Widget, bool bTranslate, bool bScale, bool bShear, bool bRotate, bool bInEaseOut, bool bInEaseIn)
: TotalTime(Duration)
, TimeElapsed(0.f)
, bInterpolating(true)
, ExecutionFunction(LatentInfo.ExecutionFunction)
, OutputLink(LatentInfo.Linkage)
, CallbackTarget(LatentInfo.CallbackTarget)
, TargetWidget(Widget)
, bInterpTranslation(bTranslate)
, InitialTranslation(FVector2D::ZeroVector)
, TargetTranslation(FVector2D::ZeroVector)
, bInterpShear(bShear)
, InitialShear(FVector2D::ZeroVector)
, TargetShear(FVector2D::ZeroVector)
, bInterpAngle(bRotate)
, InitialAngle(0.f)
, TargetAngle(0.f)
, bInterpScale(bScale)
, InitialScale(FVector2D::ZeroVector)
, TargetScale(FVector2D::ZeroVector)
, bEaseIn(bInEaseIn)
, bEaseOut(bInEaseOut)
{
}
virtual void UpdateOperation(FLatentResponse& Response) override
{
// Update elapsed time
TimeElapsed += Response.ElapsedTime();
bool bComplete = (TimeElapsed >= TotalTime);
// If we have a component to modify..
if(TargetWidget.IsValid() && bInterpolating)
{
// Work out 'Blend Percentage'
const float BlendExp = 2.f;
float DurationPct = TimeElapsed/TotalTime;
float BlendPct;
if(bEaseIn)
{
if(bEaseOut)
{
// EASE IN/OUT
BlendPct = FMath::InterpEaseInOut(0.f, 1.f, DurationPct, BlendExp);
}
else
{
// EASE IN
BlendPct = FMath::Lerp(0.f, 1.f, FMath::Pow(DurationPct, BlendExp));
}
}
else
{
if(bEaseOut)
{
// EASE OUT
BlendPct = FMath::Lerp(0.f, 1.f, FMath::Pow(DurationPct, 1.f / BlendExp));
}
else
{
// LINEAR
BlendPct = FMath::Lerp(0.f, 1.f, DurationPct);
}
}
// Update translation
if(bInterpTranslation)
{
FVector2D NewTranslation = bComplete ? TargetTranslation : FMath::Lerp(InitialTranslation, TargetTranslation, BlendPct);
TargetWidget->SetRenderTranslation(NewTranslation);
}
// Update scale
if (bInterpScale)
{
FVector2D NewScale = bComplete ? TargetScale : FMath::Lerp(InitialScale, TargetScale, BlendPct);
TargetWidget->SetRenderScale(NewScale);
}
// Update Shear
if (bInterpShear)
{
FVector2D NewShear = bComplete ? TargetShear : FMath::Lerp(InitialShear, TargetShear, BlendPct);
TargetWidget->SetRenderShear(NewShear);
}
// Update Angle
if(bInterpAngle)
{
float NewAngle = bComplete ? TargetAngle : FMath::Lerp(InitialAngle, TargetAngle, BlendPct);
TargetWidget->SetRenderAngle(NewAngle);
}
}
Response.FinishAndTriggerIf(bComplete || !bInterpolating, ExecutionFunction, OutputLink, CallbackTarget);
}
#if WITH_EDITOR
// Returns a human readable description of the latent operation's current state
virtual FString GetDescription() const override
{
return FString::Printf( *NSLOCTEXT("FInterpolateWidgetToAction", "ActionTime", "Move (%.3f seconds left)").ToString(), TotalTime-TimeElapsed);
}
#endif
};
This is a necessary class to get our latent function to work. This class takes care of updating the latent function.
Now, compile the source engine (don’t compile your project, or you will get an error message that a .dll file is busy) and once it’s done compiling, return to your own project. Here, we are going to make a BlueprintFunctionLibrary and add our own Latent function to it (or, if we already have one, just add to it).
If you don’t have your BlueprintFunctionLibrary yet, open up the editor and create yourself a new C++ class based on the “BlueprintFunctionLibrary” class. Once that is done, add the following code to it
**
MyBlueprintFunctionLibrary.h**
Add this before UCLASS()
UENUM()
namespace EMoveLatentAction
{
enum Type
{
/** Move to target over specified time. */
Move,
/** If currently moving, stop. */
Stop,
/** If currently moving, return to where you started, over the time elapsed so far. */
Return
};
}
Add this within UCLASS()
public:
UFUNCTION(BlueprintCallable, meta = (Latent, LatentInfo = "LatentInfo", WorldContext = "WorldContextObject", ExpandEnumAsExecs = "MoveAction", OverTime = "0.2"), Category = "Components")
static void MoveWidgetTo(UWidget* Widget, FVector2D TargetTranslation, FVector2D TargetScale, FVector2D TargetShear, float TargetAngle, bool bEaseOut, bool bEaseIn, float OverTime, TEnumAsByte<EMoveLatentAction::Type> MoveAction, FLatentActionInfo LatentInfo);
Now, add the following to your CPP file
MyBlueprintFunctionLibrary.cpp
Add the following includes
#include "LatentActions.h"
#include "Private/InterpolateWidgetToAction.h"
Then add the function
void UBK_BlueprintFunctionLibrary::MoveWidgetTo(UWidget* Widget, FVector2D TargetTranslation, FVector2D TargetScale, FVector2D TargetShear, float TargetAngle, bool bEaseOut, bool bEaseIn, float OverTime, TEnumAsByte<EMoveLatentAction::Type> MoveAction, FLatentActionInfo LatentInfo)
{
if (UWorld* World = ((Widget != NULL) ? Widget->GetWorld() : NULL))
{
FLatentActionManager& LatentActionManager = World->GetLatentActionManager();
FInterpolateWidgetToAction* Action = LatentActionManager.FindExistingAction<FInterpolateWidgetToAction>(LatentInfo.CallbackTarget, LatentInfo.UUID);
const FVector2D WidgetTranslation = (Widget != NULL) ? Widget->RenderTransform.Translation : FVector2D::ZeroVector;
const FVector2D WidgetScale = (Widget != NULL) ? Widget->RenderTransform.Scale : FVector2D::ZeroVector;
const FVector2D WidgetShear = (Widget != NULL) ? Widget->RenderTransform.Shear : FVector2D::ZeroVector;
const float WidgetAngle = (Widget != NULL) ? Widget->RenderTransform.Angle : 0.f;
const bool bTranslate = (Widget != NULL) ? Widget->RenderTransform.Translation != TargetTranslation : false;
const bool bScale = (Widget != NULL) ? Widget->RenderTransform.Scale != TargetScale : false;
const bool bShear = (Widget != NULL) ? Widget->RenderTransform.Shear != TargetShear : false;
const bool bRotate = (Widget != NULL) ? Widget->RenderTransform.Angle != TargetAngle : false;
// If not currently running
if (Action == NULL)
{
if (MoveAction == EMoveLatentAction::Move)
{
// Only act on a 'move' input if not running
Action = new FInterpolateWidgetToAction(OverTime, LatentInfo, Widget, bTranslate, bScale, bShear, bRotate, bEaseIn, bEaseOut);
Action->TargetTranslation = TargetTranslation;
Action->TargetScale = TargetScale;
Action->TargetShear = TargetShear;
Action->TargetAngle = TargetAngle;
Action->InitialTranslation = WidgetTranslation;
Action->InitialScale = WidgetScale;
Action->InitialShear = WidgetShear;
Action->InitialAngle = WidgetAngle;
LatentActionManager.AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID, Action);
}
}
else
{
if (MoveAction == EMoveLatentAction::Move)
{
// A 'Move' action while moving restarts interpolation
Action->TotalTime = OverTime;
Action->TimeElapsed = 0.f;
Action->TargetTranslation = TargetTranslation;
Action->TargetScale = TargetScale;
Action->TargetShear = TargetShear;
Action->TargetAngle = TargetAngle;
Action->InitialTranslation = WidgetTranslation;
Action->InitialScale = WidgetScale;
Action->InitialShear = WidgetShear;
Action->InitialAngle = WidgetAngle;
}
else if (MoveAction == EMoveLatentAction::Stop)
{
// 'Stop' just stops the interpolation where it is
Action->bInterpolating = false;
}
else if (MoveAction == EMoveLatentAction::Return)
{
// Return moves back to the beginning
Action->TotalTime = Action->TimeElapsed;
Action->TimeElapsed = 0.f;
// Set our target to be our initial, and set the new initial to be the current position
Action->TargetTranslation = Action->InitialTranslation;
Action->TargetScale = Action->InitialScale;
Action->TargetShear = Action->InitialShear;
Action->TargetAngle = Action->InitialAngle;
Action->InitialTranslation = WidgetTranslation;
Action->InitialScale = WidgetScale;
Action->InitialShear = WidgetShear;
Action->InitialAngle = WidgetAngle;
}
}
}
}
Now you should be good to go! Compile and open your project in the editor. Now, when you want to move something like an image in your interface, just use this latent function! Even though there’s technically a timeline available in UMG, I thought that sometimes it is easier to use this latent action. It can also be useful because you can use EaseIn and EaseOut (for instance, when you want interface elements to move in or out of the screen when opening/closing the interface). To give proper credit where due; this code is largely based on the MoveComponentTo code, with alterations to change the component to a widget and to change the location and rotation to the widget’s rendertransform.
If there are any problems, please let me know in this thread.