Hey, everybody! I can’t believe it’s already been four months since I released the Crow Down Showdown demo! 😮 A huge thanks to everyone who has played it, sent me a nice comment or shared it with their friends! 😀

In this blog post, I will talk about one of the many cool things I developed for the game: My C++ Widget animation system! It’s gonna be a more technical post aimed at programmers, but everyone is welcome to read, just be warned that you might become one of us! I do not recommend, however, reading this from a phone, or you’ll have a bad time reading all the code! /o\

Still with me? Then let’s begin!

The first thing someone may think of when the see the words “Animation” and “C++” together is “wHy wOuLd AnYoNe UsE C++ fOr AnImAtIoNs WhEn YoU cAn CrEaTe ThEm ViSuAlLy WiTh KeYfRaMeS iN A TiMeLiNe?”. And while timelines are great when you want to play an animation that is exactly the same every time, if you need something dynamic, with parameters that affect your animation’s behavior, they’re not a very good fit for the job. And that is exactly what I needed to do for my “Objective complete” animation, which you can see below:

As you can see, the animation consists of multiple transitions where icons stylishly fly across the screen from the Current Obstacle Counter at the top towards the icon of the matching Finished Obstacle at the bottom. Both the start and end positions of each animation are dynamic, so using timelines would’ve required a lot of repetition to cover every possible case, and since I’m not insane (at least not in this context) I had to come up with a flexible solution using a custom Latent Action.

So what is a Latent Action? You’ve probably already used some of them! They are these fancy Blueprint nodes with a Clock symbol in their top right, indicating that at least one of their output execution pins will have a delayed response. Here are some examples:

My plan was to create a Latent Action that could move a widget to a target position with a single Blueprint node, and then continue the execution once that animation was finished. Then, I could execute it for every widget that I would like to animate with different parameters. Before I explain how I did it, here’s a preview of what the final node looks like:

I’ll be going over what each of the input and output pins do later.

So how do you even make something like that? At first, I had no idea, but one of the best ways to learn how to do things in Unreal is by checking the engine’s source code!


Part 1: Learning from the Delay Node

Since the Delay node is the simplest Latent Action in Unreal, it was a great reference to learn how to make my own Latent Actions. By hovering over a Blueprint Node that was implemented in C++, you can see where its declaration comes from in the last line of the tooltip, after “Target is”.

As you can see, this node comes from Kismet System Library, or more specifically, “KismetSystemLibrary.h”. These files can get pretty large, so if you have trouble finding a specific function, the easiest way is to search for the description you found in its tooltip, which should be in a comment above the function’s declaration!

So we are going on a fun trip to KismetSystemLibrary.h! A lot of global Blueprint nodes come from this file. Here’s what the Delay node’s declaration looks like:

/** 
 * Perform a latent action with a delay (specified in seconds).  Calling again while it is counting down will be ignored.
 * 
 * @param  WorldContext   World context.
 * @param  Duration      length of delay (in seconds).
 * @param  LatentInfo    The latent action.
 */
UFUNCTION(BlueprintCallable, Category ="Utilities|FlowControl", meta= (Latent, WorldContext="WorldContextObject", LatentInfo ="LatentInfo", Duration= "0.2", Keywords="sleep"))
static void  Delay(const  UObject*  WorldContextObjectfloat  Durationstruct  FLatentActionInfo LatentInfo);

Let’s break this declaration down! For now, we’ll ignore the UFUNCTION macro and go straight to basic C++.

  • Like every other library function, it’s static.
  • UObject* WorldContextObject is what allows us to get our current World, even though we don’t have an Actor to invoke GetWorld().
  • float Duration is how long the Delay will last in seconds.
  • struct FLatentActionInfo LatentInfo contains useful information that allows our Latent Action to behave as expected, as you will soon see in the function’s definition.

How come there are three parameters in the C++ function, but only one (Duration) in the Delay Blueprint node? That’s where the UFUNCTION macro comes in:

  • UFUNCTION indicates that this function will be recognized by Unreal’s reflection system, which is required not only for Blueprints, but also other fancy things like delegate binding and network callbacks, which we’ll save for another time;
  • BlueprintCallable is pretty self-explanatory, as it allows this function to be called from Blueprints;
  • Category="Utilities|FlowControl" helps make the function easier to find in the Blueprint Editor, which is always a good practice. The vertical slash (|) is used to create sub-categories, which is great for large libraries like this one!
  • Latent defines the node as a Latent Action (very important!);
  • WorldContext="WorldContextObject" and LatentInfo="LatentInfo" let Unreal know which of our parameters will receive the relevant WorldContext and LatentInfo values, respectively. These parameters will be set automatically without showing up in our Blueprint node!
  • Duration="0.2" simply gives the Duration parameter a default value;
  • Keywords="sleep" means that Unreal will also suggest this function if you search for “sleep” in the Blueprint editor.

Let’s take a moment to read its definition:

void UKismetSystemLibrary::Delay(const UObject* WorldContextObjectfloat DurationFLatentActionInfo LatentInfo)
{
    if (UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull))
    {
        FLatentActionManager& LatentActionManager = World->GetLatentActionManager();
        if (LatentActionManager.FindExistingAction<FDelayAction>(LatentInfo.CallbackTargetLatentInfo.UUID== NULL)
        {
            LatentActionManager.AddNewAction(LatentInfo.CallbackTargetLatentInfo.UUIDnew FDelayAction(Duration, LatentInfo));
        }
    }
}

Now we can see those parameters in action! WorldContextObject is used to get the current World, which is used to get a LatentActionManager. That Action Manager is then used to create and keep track of a new FDelayAction with the specified Duration, unless an Action with the same CallbackTarget and UUID (Universally Unique Identifier) already exists, which is what would happen if you called the same Delay node a second time before its first execution finished. If you’re curious, you can also check out the code for RetriggerableDelay, which is almost identical to this one, except it resets any existing FDelayAction’s TimeRemaining to the current value of Duration.

So far, our MoveWidgetTo function will be extremely similar to what we’ve seen. Just like the Delay function, it’ll need a Duration, WorldContextObject and LatentInfo, so it can create an Action and add it to the LatentActionManager. So what are these actions I keep mentioning, after all? Their full name is FPendingLatentAction, and they contain the Update logic that happens behind the scenes while you are waiting for your Blueprint node to finish executing.

Unreal’s Delay node uses its own subclass of FPendingLatentAction, which is called FDelayAction. Let’s check it out!

// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
 
#include "CoreMinimal.h"
#include "UObject/WeakObjectPtr.h"
#include "Engine/LatentActionManager.h"
#include "LatentActions.h"
 
// FDelayAction
// A simple delay action; counts down and triggers its output link when the time remaining falls to zero
class FDelayAction  : public FPendingLatentAction
{
public:
    float TimeRemaining;
    FName ExecutionFunction;
    int32 OutputLink;
    FWeakObjectPtr CallbackTarget;
 
    FDelayAction(float Durationconst FLatentActionInfo& LatentInfo)
        : TimeRemaining (Duration)
        , ExecutionFunction (LatentInfo.ExecutionFunction)
        , OutputLink (LatentInfo.Linkage)
        , CallbackTarget (LatentInfo.CallbackTarget)
    {
    }
 
    virtual void UpdateOperation(FLatentResponse& Responseoverride
    {
        TimeRemaining -= Response.ElapsedTime();
        Response.FinishAndTriggerIf(TimeRemaining <= 0.0f, ExecutionFunction, OutputLink, CallbackTarget);
    }
 
#if WITH_EDITOR
    // Returns a human readable description of the latent operation's current state
    virtual FString GetDescription (const override
    {
        static const FNumberFormattingOptions DelayTimeFormatOptions = FNumberFormattingOptions()
            .SetMinimumFractionalDigits (3)
            .SetMaximumFractionalDigits (3);
        return FText::Format(NSLOCTEXT("DelayAction""DelayActionTimeFmt""Delay ({0} seconds left)"), FText::AsNumber(TimeRemaining, &DelayTimeFormatOptions)).ToString();
    }
#endif
};
 

We can learn from this that our custom Action class will need these things:

  • To inherit from FPendingLatentAction;
  • A constructor that initializes any variables that my Action will need, including copies of some of FLatentActionInfo‘s fields, so we can quickly access them later;
  • The following overridden functions:
    • void UpdateOperation(FLatentResponse& Response), where we run our update logic on every frame and notify when we are finished.
    • FString GetDescription(), which is for debugging purposes and only exists when playing from the Unreal Editor. It would be a much shorter function if not for localization and formatting, but it’s still pretty simple.
Here’s what the Description from GetDescription() looks like when debugging your Blueprints!

Now that we know everything about how the Delay node works, we can finally start getting our hands dirty and implement our MoveWidgetTo node!


Part 2: First Implementation of MoveWidgetTo

The first thing we’re gonna need is a Library class where we can create our function and expose it to Blueprints. If you don’t have one of those yet, you can create one from the Unreal Editor by going to File > New C++ Class, and then selecting Blueprint Function Library as your parent class:

So here’s what an early version of the MoveWidgetTo function looked like in my BlueprintUtils.h file:

// BlueprintUtils.h
 
UFUNCTION(BlueprintCallable, Category = "Widget Transitions", meta = (Latent, WorldContext = "WorldContextObject", LatentInfo = "LatentInfo"))
static void MoveWidgetTo(UWidget* Widgetconst FVector2D TargetPositionfloat TransitionTimeUObject* WorldContextObjectstruct FLatentActionInfo LatentInfo);

Not too different from Delay(). I renamed Duration to TransitionTime, added a pointer to a UWidget and an FVector2D TargetPosition, which should be enough for a simple transition. The final version had some extra parameters, which we’ll go back to later.

And here is its implementation, with some comments I added just for this post:

// BlueprintUtils.cpp
 
void UBlueprintUtils::MoveWidgetTo(UWidget* Widgetconst FVector2D TargetPositionfloat TransitionTimeUObject* WorldContextObjectstruct FLatentActionInfo LatentInfo)
{
    // Since we cannot move a Widget without a Widget, I added this
    // null check below with an error message.
    if (Widget == nullptr)
    {
        UE_LOG(LogTemp, Error, TEXT("MoveWidgetTo was called with a null Widget"));
    }
 
    // The next 3 lines were copied from the Delay function and not changed at all.
    if (UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull))
    {
        FLatentActionManager& LatentActionManager = World->GetLatentActionManager();
 
        // Unlike the Delay function, we don't need to check if an Action is already
        // running on this node with LatentActionManager.FindExistingAction().
        // If this node gets called a second time while it's already running, I want
        // BOTH transitions to finish executing, not for the new one to be ignored,
        // or for the old one to be interrupted. This allows me to iterate through
        // multiple Widgets and animate all of them with a single MoveWidgetTo Node.
 
        // Instead of creating an FDelayAction, we create an FMoveWidgetTransitionAction,
        // a class which we'll implement in the next section.
        FMoveWidgetTransitionAction* NewAction = new FMoveWidgetTransitionAction(
            Widget,
            TargetPosition,
            TransitionTime,
            LatentInfo
        );
 
        // Finally, we can add our fancy new Action to the LatentActionManager,
        // and let it handle things from here!
        LatentActionManager.AddNewAction(LatentInfo.CallbackTargetLatentInfo.UUID, NewAction);
    }
}
This is what our node looks like so far! It’s pretty, but it doesn’t do anything yet.

Now let’s get to the most important part, the FMoveWidgetTransitionAction class!

#pragma once
 
#include "CoreMinimal.h"
#include "UObject/WeakObjectPtr.h"
#include "Engine/LatentActionManager.h"
#include "LatentActions.h"
#include <UMG/Public/Components/Widget.h>
#include <Vector2D.h>
#include "Kismet/KismetMathLibrary.h"
#include <Kismet/GameplayStatics.h>
 
// As previously mentioned, our class must be based on FPendingLatentAction.
class FMoveWidgetTransitionAction  : public FPendingLatentAction
{
public:
    // Original parameters:
    UWidget* Widget;
    const FVector2D TargetPosition;
    float Duration;
    
    // New variables that we need:
    const FVector2D InitialPosition; // We need to save the Widget's InitialPosition for Lerping purposes
    float ElapsedTime = 0;
 
    // Fields copied from LatentInfo:
    FName ExecutionFunction;
    int32 OutputLink;
    FWeakObjectPtr CallbackTarget;
 
    FMoveWidgetTransitionAction(UWidget* Widgetconst FVector2D TargetPositionfloat TransitionTimeconst FLatentActionInfo& LatentInfo)
        : Widget(Widget)
        , InitialPosition (Widget->RenderTransform.Translation)
        , TargetPosition (TargetPosition)
        , Duration(TransitionTime)
        , ExecutionFunction (LatentInfo.ExecutionFunction)
        , OutputLink (LatentInfo.Linkage)
        , CallbackTarget (LatentInfo.CallbackTarget)
    {
    }
 
    virtual void UpdateOperation(FLatentResponse& Responseoverride
    {
        // First, we update ElapsedTime, which is similar to
        // what FDelayAction did with TimeRemaining, except
        // we are adding Time instead of subtracting it.
        ElapsedTime += Response.ElapsedTime();
 
        // Now, we must determine the Widget's position for this frame,
        // starting with a clamped alpha value for our interpolation.
        float alpha = FMath::Clamp(ElapsedTime / Duration, 0.f1.f);
 
        // Now we can interpolate between our widget's initial position and target position.
        float x = UKismetMathLibrary::Lerp(InitialPosition.XTargetPosition.X, alpha);
        float y = UKismetMathLibrary::Lerp(InitialPosition.YTargetPosition.Y, alpha);
 
        // Finally, we can update our Widget's position! \o/
        Widget->SetRenderTranslation(FVector2D(x, y));
 
        // And let the Action be considered finished when (alpha >= 1).
        // Because of our Clamp, alpha should never exceed 1, but it doesn't hurt
        // to be a bit paranoid when working with float values.
        Response.FinishAndTriggerIf(alpha >= 1.0f, ExecutionFunction, OutputLink, CallbackTarget);
    }
 
#if WITH_EDITOR
    // Returns a human readable description of the latent operation's current state.
    virtual FString GetDescription (const override
    {
        static const FNumberFormattingOptions TransitionTimeFormatOptions = FNumberFormattingOptions()
            .SetMinimumFractionalDigits(3)
            .SetMaximumFractionalDigits(3);
        
        // This function is mostly unchanged from FDelayAction. I just updated the strings
        // below to say "MoveWidgetTransition" instead of "Delay", and replaced the TimeRemaining 
        // variable with (Duration - ElapsedTime), which is effectively the same thing.
        return FText::Format(NSLOCTEXT("FMoveWidgetTransitionAction""MoveWidgetTransitionActionTimeFmt""Move Widget Transition ({0} seconds left)"), FText::AsNumber(Duration - ElapsedTime, &TransitionTimeFormatOptions)).ToString();
    }
#endif
};

If you’ve manage to follow everything so far, congrats! You can already use this knowledge to create your own Widget transitions with Latent Actions!


Part 3: Refinements

What if I told you that what we’ve done so far isn’t good enough yet? What would you add to our functions and FMoveWidgetTransitionAction class to make them even better?

First of all, our transition is completely linear, which is not juicy at all. Do you know how we could easily fix that? Exactly! With an easing function. We could even expose that parameter to our Blueprint node for maximum customization. But do you know what’s better than one easing function? TWO easing functions! Why? So we can have one for each axis, creating all sorts of interesting combinations! Trust me, you’re gonna love this! 👀

If you use the EEasingFunc::Type enum you get a cool dropdown selector in your Blueprint node!

We need to add these two parameters to our MoveWidgetTo function: EEasingFunc::Type XEasing and EEasingFunc::Type YEasing:

UFUNCTION(BlueprintCallable, Category = "Widget Transitions", meta = (Latent, WorldContext = "WorldContextObject", LatentInfo = "LatentInfo"))
static void MoveWidgetTo(UWidget* Widgetconst FVector2D TargetPositionfloat TransitionTimeconst EEasingFunc::Type XEasingconst EEasingFunc::Type YEasingUObject* WorldContextObjectstruct FLatentActionInfo LatentInfo);

Then, by doing the same thing we did with our other parameters (passing them to our FMoveWidgetTransitionAction constructor, which then copies them to a variable), we can finally replace this part of our UpdateOperation function:

float x = UKismetMathLibrary::Lerp(InitialPosition.XTargetPosition.X, alpha);
float y = UKismetMathLibrary::Lerp(InitialPosition.YTargetPosition.Y, alpha);

with this:

float x = UKismetMathLibrary::Ease(InitialPosition.XTargetPosition.X, alpha, XEasing);
float y = UKismetMathLibrary::Ease(InitialPosition.YTargetPosition.Y, alpha, YEasing);

Let me show you a comparison of what my animation looked like before and after replacing Lerp with Ease, as well an alternative version with different parameters:

Before using Ease, we had a simple Lerp.
This is equivalent to using a Linear easing for both axes.
XEasing = Sinusoidal Out
YEasing = Sinusoidal In
By using different easing functions for each axis, we can create nice curved movements like this!
XEasing = Sinusoidal In
YEasing = Sinusoidal Out
This is the one that I ended up using! I really like how it looks!

Now what if I wanted to do something to a widget after its transition finished? Since I am iterating through multiple Widgets and animating all of them with a single MoveWidgetTo node, I need to know which Widget just finished animating when the Completed execution output pin is triggered. In my case, so I can delete it and not end up with multiple copies of the same widget in the same target location. If we assume that every widget has the same transition time, we could just iterate through the same widgets to know which one just finished animating. Although that would work on this case, it wouldn’t be a very reliable way to know which widget finished animating. Thankfully, we can add an output node to do exactly that for us!

So do we just make the MoveWidgetTo C++ function return something instead of nothing? That wouldn’t work, since it would return the widget as soon as the function finished executing, *not* when the animation actually finished, which depends on the Latent Action we created.

But there is another way! When working with C++ functions exposed to Blueprints, any parameter with the Reference (&) character becomes an output node (which is what allows us to have multiple output pins in the first place).

So all we need to do is add a UWidget*& CompletedWidget parameter to our function, and the Blueprint node will get a UWidget* output pin… We can use this reference to set that output value whenever we want!

UFUNCTION(BlueprintCallable, Category = "Widget Transitions", meta = (Latent, WorldContext = "WorldContextObject", LatentInfo = "LatentInfo"))
static void MoveWidgetTo(UWidget* WidgetUWidget*& CompletedWidgetconst FVector2D TargetPositionfloat TransitionTimeconst EEasingFunc::Type XEasingconst EEasingFunc::Type YEasingUObject* WorldContextObjectstruct FLatentActionInfo LatentInfo);

So we pass the CompletedWidget reference to our FMoveWidgetTransitionAction, where it’ll be copied to another variable of the same type and name…

FMoveWidgetTransitionAction* NewAction = new FMoveWidgetTransitionAction(
    Widget,
    CompletedWidget,
    TargetPosition,
    TransitionTime,
    XEasing,
    YEasing,
    LatentInfo
);

And finally, we update CompletedWidget right before the transition is finished at the end of the UpdateOperation function:

virtual void UpdateOperation (FLatentResponse& Responseoverride
{
    // ...
        
    if (alpha >= 1.f)
    {
        CompletedWidget = Widget;
    }
 
    Response.FinishAndTriggerIf(alpha >= 1.0f, ExecutionFunction, OutputLink, CallbackTarget);
}

References are really useful, huh? Check out the added “Completed Widget” output pin in the screenshot below:

Now I can use this to remove a widget once its transition is complete! \o/ If I tried to use the “Transitioning Widget” variable that I used as an input for the MoveWidgeTo node instead, its value wouldn’t be the same by the time I called Remove from Parent, since I used a Sequence with a delay in a loop to trigger multiple transitions without having to wait for any of them to end.

If your game messes with time dilation for slow-motion effects, you may or may not want your widget transitions to be affected by the global time dilation. The DeltaTime you get from FLatentResponse’s ElapsedTime() in the UpdateOperation function is influenced by the current global time dilation, so if you want to compensate for that, you have to divide ElapsedTime() by the time dilation, like this:

// I went ahead and added this variable here and in the MoveWidgetTo function to toggle this new behavior.
if (bIgnoreGlobalTimeDilation)
{
    float timeDilation = UGameplayStatics::GetGlobalTimeDilation(Widget->GetWorld());
    ElapsedTime += Response.ElapsedTime() / timeDilation; // timeDilation must not be zero!
}
else
{
    ElapsedTime += Response.ElapsedTime();
}

And that’s it! Now we have a much improved MoveWidgetTo Blueprint node, which we can use in all sorts of animations:

While I used this node for my Obstacle Class Completed animation, which is something very specific to my game, a lot of games have similar animations when you complete a certain objective and a bunch of collectibles fly across the screen (whether they are Wumpa fruits, lives, gems, coins, or whatever).

Not only that, but we’ve seen what a powerful tool Latent Actions can be, and I hope this post may have served as a nice introduction to them! We’ve only really scratched the surface of what you can do with them, so the next time you see a cool Latent Node (perhaps a more complex one, with multiple output execution pins), be sure to check its source code to see how it works! Curiosity is on your side!

Since I didn’t want this post to get super long, I skipped some of the repetitive tasks like adding variables to FMoveTransitionAction and its constructor while explaining these refinements, but if you’d like to review or copy the finished versions of my functions and classes, I’ve pasted them below in their full glory. Just don’t forget to check the bonus section after them! 👀

// BlueprintUtils.h
 
UFUNCTION(BlueprintCallable, Category = "Widget Transitions", meta = (Latent, WorldContext = "WorldContextObject", LatentInfo = "LatentInfo"))
static void MoveWidgetTo(UWidget* WidgetUWidget*& CompletedWidgetconst FVector2D TargetPositionfloat TransitionTimeconst EEasingFunc::Type XEasingconst EEasingFunc::Type YEasingbool bIgnoreGlobalTimeDilationUObject* WorldContextObjectstruct FLatentActionInfo LatentInfo);
// BlueprintUtils.cpp
 
void UBlueprintUtils::MoveWidgetTo(UWidget* WidgetUWidget*& CompletedWidgetconst FVector2D TargetPositionfloat TransitionTimeconst EEasingFunc::Type XEasingconst EEasingFunc::Type YEasingbool bIgnoreGlobalTimeDilationUObject* WorldContextObjectstruct FLatentActionInfo LatentInfo)
{
    if (Widget == nullptr)
    {
        UE_LOG( LogTemp, Error, TEXT("MoveWidgetTo was called with a null Widget"));
    }
 
    if (UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull))
    {
        FLatentActionManager& LatentActionManager = World->GetLatentActionManager();
 
        FMoveWidgetTransitionAction* NewAction = new FMoveWidgetTransitionAction(
            Widget,
            CompletedWidget,
            TargetPosition,
            TransitionTime,
            XEasing,
            YEasing,
            bIgnoreGlobalTimeDilation,
            LatentInfo
        );
        LatentActionManager.AddNewAction(LatentInfo.CallbackTargetLatentInfo.UUID, NewAction);
    }
}
// MoveWidgetTransitionAction.h
 
#pragma once
 
#include "CoreMinimal.h"
#include "UObject/WeakObjectPtr.h"
#include "Engine/LatentActionManager.h"
#include "LatentActions.h"
#include <UMG/Public/Components/Widget.h>
#include <Vector2D.h>
#include "Kismet/KismetMathLibrary.h"
#include <Kismet/GameplayStatics.h>
 
class FMoveWidgetTransitionAction  : public FPendingLatentAction
{
public:
    UWidget* Widget;
    UWidget*& CompletedWidget;
    const FVector2D InitialPosition;
    const FVector2D TargetPosition;
    float Duration;
    float ElapsedTime = 0;
    EEasingFunc ::Type XEasing;
    EEasingFunc ::Type YEasing;
    bool bIgnoreGlobalTimeDilation;
    FName ExecutionFunction;
    int32 OutputLink;
    FWeakObjectPtr CallbackTarget;
 
    FMoveWidgetTransitionAction(UWidget* WidgetUWidget*& CompletedWidgetconst FVector2D TargetPositionfloat TransitionTimeEEasingFunc::Type XEasingEEasingFunc::Type YEasingbool bIgnoreGlobalTimeDilationconst FLatentActionInfo& LatentInfo)
        : Widget(Widget)
        , CompletedWidget (CompletedWidget)
        , InitialPosition (Widget->RenderTransform.Translation)
        , TargetPosition (TargetPosition)
        , Duration(TransitionTime)
        , XEasing (XEasing)
        , YEasing (YEasing)
        , bIgnoreGlobalTimeDilation(bIgnoreGlobalTimeDilation)
        , ExecutionFunction (LatentInfo.ExecutionFunction)
        , OutputLink (LatentInfo.Linkage)
        , CallbackTarget (LatentInfo.CallbackTarget)
    {
    }
 
    virtual void UpdateOperation(FLatentResponse& Responseoverride
    {
        if (bIgnoreGlobalTimeDilation)
        {
            float timeDilation = UGameplayStatics::GetGlobalTimeDilation(Widget->GetWorld()); // Global time dilation must not be zero!
            ElapsedTime += Response.ElapsedTime() / timeDilation;
        }
        else
        {
            ElapsedTime += Response.ElapsedTime();
        }
 
        float alpha = FMath::Clamp(ElapsedTime / Duration, 0.f1.f);
 
        float x = UKismetMathLibrary::Ease(InitialPosition.XTargetPosition.X, alpha, XEasing);
        float y = UKismetMathLibrary::Ease(InitialPosition.YTargetPosition.Y, alpha, YEasing);
 
        Widget->SetRenderTranslation (FVector2D(x, y));
        
        if (alpha >= 1.f)
        {
            CompletedWidget = Widget;
        }
 
        Response.FinishAndTriggerIf(alpha >= 1.0f, ExecutionFunction, OutputLink, CallbackTarget);
    }
 
#if WITH_EDITOR
    // Returns a human readable description of the latent operation's current state
    virtual FString GetDescription (const override
    {
        static const FNumberFormattingOptions TransitionTimeFormatOptions = FNumberFormattingOptions()
            .SetMinimumFractionalDigits (3)
            .SetMaximumFractionalDigits (3);
        
        return FText::Format(NSLOCTEXT("FMoveWidgetTransitionAction""MoveWidgetTransitionActionTimeFmt""Move Widget Transition ({0} seconds left)"), FText::AsNumber(Duration - ElapsedTime, &TransitionTimeFormatOptions)).ToString();
    }
#endif
};

Bonus: My Blueprint Animation Setup

While this post was focused on the C++ side of things, in case any of you are curious about how exactly I used the MoveWidgetTo node in Blueprints for my Obstacle Class Completed animation, I didn’t want to leave you wondering, so click here to open a full-sized screenshot of what the entire Macro for that looks like (plus a bonus function to get a Widget’s position relative to another widget!). It’s a pretty wide image, so it may take a while to load! I was going to use BlueprintUE.com to share them, but it messed with the formatting a bit too much this time, so I thought a screenshot would be better. I used the Graph Printer plugin, by the way, which is free in the Unreal Marketplace. No, they are not the sponsor of today’s blog post. I genuinely think it’s a great plugin, but YOU can be my sponsor if you want, by supporting me on Ko-fi by clicking on the big blue button which you can find at the bottom of this page.

PS: If anyone knows a good way to share C++ widgets with syntax highlighting in HTML, please let me know! I didn’t like WordPress’ SyntaxHighlighter block since it didn’t highlight much and only had a light theme, so I ended up pasting my code into VS Code, copying it with syntax highlighting to Microsoft Word, saving it as HTML, and then embedding each snippet in a custom HTML block (which also required some CSS changes). It worked and I like how it ended up looking, but it is a pretty annoying process to go back and edit things now, so I would like to find a better solution for the next time I share C++ in a post.

Thank you for reading, and stay Epic, my friends! :v

Leave a comment

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: