A common way to handle weapons is to attach them to different sockets of your skeleton. Drawing and sheathing your favorite gang buster is handled by switching the attachment thereof at a specific frame in your animations. Unless you have pixel-perfect animations, there will be a visible snap. Here is the before and after my approach to this problem:

Visible snapping
Transitioning smoothly

The idea

There are three (or more) ways to handle this:

  • Change your animations, per character! Unfeasible for most developers if reusability is a concern and different proportions are a thing
  • Use inverse kinematics to tweak your animations procedurally! Feasible, but a lot of work to fine-tune the IK alpha for every animation.
  • Just lerp the sword to where it should be! As long as the original and target transforms are close to each other, a lerp does wonders.

I decided to go for the third option, lerping. Lerping means interpolating linearly between two values (here: transforms). We are basically animating the sword transform over a short period of time to transition from the original transform to the target.

This is far from being original. But anything time-sensitive can’t really be handled in components. Components can’t have timelines, because timelines are components themselves. There are different ways to achieve this, but all of them have, in my opinion, serious drawbacks.

This is where async Blueprint nodes come in. You can write these in C++ only, and they allow you to encapsulate time sensitive functionality without relying on timelines in actors, event tick or Blueprint timers. There will still be some of that internally, but it’s hidden from the Blueprint layer, making it nice and tidy. Beware:

Anticlimatic, right? Isn’t that beautiful?

This node takes in a single scene component (a mesh for example) and a new socket name, and will automatically lerp the component to the new socket. The duration is configurable, so it’s very easy to test out different values. I found 0.2 to be a good value.

The code principles

First things first. To create an async Blueprint node, we create a class inheriting from UBlueprintAsyncActionBase. The code is found at the bottom, in case you want to see it before reading the explanation (or you just don’t require an explanation).

The general outline is as follows:

  1. Instantiate an object of the class using Blueprints
  2. Start a repeating process using the virtual Activate function
  3. The repeating process repeats until a goal is achieved
  4. Optionally broadcast a multicast delegate
  5. Mark the object as ready to be destroyed

We have to write two functions at the very least:

  • The virtual Activate function has to be overriden and is responsible for initial setup and calculations. In other tutorials, a timer is registered in this function. We are not doing this. Also, we need to call the RegisterWithGameInstance function. This ensures the life of our async object for as long as we need. Once the object has served its purpose, we get rid of it by calling SetReadyToDestroy.
  • A static BlueprintCallable creation function. It instantiates an object of the class we are currently writing and initializes it. This is the function we are actually calling in Blueprints.

There is a very important reason why we aren’t using a timer. When we are lerping, we have to consider DeltaTime. as we don’t want frame rate to influence the speed at which we are lerping. A timer has to be given an interval. If we were to assume a specific interval, we couldn’t use the world’s DeltaTime for the lerping calculations. DeltaTime here would need to be our interval. Gladly, there is a better solution that integrates more naturally with the world’s DeltaTime.

We let our class inherit from a second class, FTickableGameObject. This gives us three additional virtual functions:

  • Tick(float DeltaTime). This gives us the DeltaTime. Since the Activate function has to come before the Tick, we have a boolean to determine whether Tick should be called or not. We simply set it to true in Activate in addition to our general setup. We set the bool back to false once the node has finished its purpose.
  • IsTickable(). Here, we simply return the bool property we created. This has the added side effect of excluding the Class Default Object (CDO) from being ticked, as the Activate function never gets called on it, so the bool property never allows the CDO to tick.
  • GetStatId(). We need to implement this, as the parent function is pure. This is for stat tracking purposes, but I haven’t actually looked into it. Just use the macro RETURN_QUICK_DECLARE_CYCLE_STAT(FTest, STATGROUP_Tickables). FTest should be named appropriately.

Adding execution pins (optional)

Sometimes, we want to be informed of a specific outcome of the node. Maybe it can get interrupted, or we want to execute additional logic once it finishes.

To add execution (output) pins, we have to create a multicast delegate (in Blueprint known as event dispatchers). They need to be multicast, as Blueprint only supports multicast delegates. Simply add an instance of each of your delegates in your class, and make them BlueprintAssignable. That’s it!
This works because in the graph node class that was pre-written by Epic iterates over all multicast delegate properties of the class to create new ‘then’ exec pins.

Returning the async task as object (optional)

By default, you don’t have access to the instantiated object anymore. You only have the exec pins as defined by your delegates. However, in some cases it can be useful to still be able to access your async object. What if something should cancel the async task, or its behavior should change due to a specific condition being fulfilled?
You can expose the async task object by defining a UCLASS meta tag called ExposedAsyncProxy. This lets you specify the name of the output pin of the node. The example used here:

UCLASS(meta = (ExposedAsyncProxy = LerpAsyncAction))
class YOURMODULE_API ULerpToNewAttachmentSocketProxy : public UBlueprintAsyncActionBase, public FTickableGameObject

This has the benefit that you can write additional BlueprintCallable functions to use. Here, I added a simple DrawDebug function that lets us draw a debug sphere at the current location. We call this every time in Blueprints when the transform is updated from the Tick function.

The lerp

So far, we only really learned of some of the things to consider or be aware of when writing a Blueprint async node. Now for an explanation of the actual function contents in the order of calling:

  1. Activate: We immediately attach the scene component to the new target socket and keep the world transform. This will make the scene component have the new socket transform as its origin, but due to changes in transform values it will still be at the same spot and have the same orientation and scale as before. This has the advantage that the target transform is now known due to becoming relative: it’s 0,0,0 for location, 0,0,0 for rotation, and 1, 1, 1 for scale. If the scene component assumes these values, it will have reached the target socket’s transform. The idea is that all our calculations will ultimately be in that space.

    The choice of this space instead of the start socket space is important: if we left the component attached to the start socket until the lerp is finished, the starting transform would continue having an influence on the component while lerping. We also can’t use world space. In world space, if our character was falling, the starting position would be stuck in world space. Our sword would be lerping from a place far above us!

    Since the component is now relative to the target socket, we have to get the starting transform in target space too, so we can interpolate properly. We do this by using StartTransformRelative = SceneComponent->GetRelativeTransform();
    Since we just attached the scene component to the new socket using KeepWorldTransform, the relative transform of the scene component is already what we are looking for.
    Now we have both start and target transforms in target socket space.
  2. IsTickable: Since we have set the bool that allows us to tick to true in the Activate function, IsTickable now returns true and allows us to tick.
  3. Tick: The actual lerp happens here, and we check for goal completion.
    First, we build our Alpha that drives the interpolation, by dividing CurrentDuration/Duration. The current duration is accumulated delta times, the duration is the parameter we passed into the object when we initialized it. We also clamp the Alpha between 0 and 1. This is important, since the division could result in a value greater than 1. We then use the following function
    const FTransform NewTransformInRelative = UKismetMathLibrary::TLerp(StartTransformRelative, FTransform(), Alpha, ELerpInterpolationMode::DualQuatInterp);
    to interpolate from out start transform to our target transform and apply this new relative transform each frame. After the specified duration has passed, the scene component will have the correct transform. After applying the transform, we update the accumulated time by adding the current DeltaTime to it, and we call our OnUpdated delegate. To finish things up, we check if the accumulated time is equal or has already surpassed the maximum duration, and if so, we fire off our OnFinished delegate, which in turn will execute the pin associated with this delegate to trigger your dependant logic.

The code

Header:

DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnFinished);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnUpdated);

UCLASS(meta = (ExposedAsyncProxy = LerpAsyncAction))
class YOURMODULE_API ULerpToNewAttachmentSocketProxy : public UBlueprintAsyncActionBase, public FTickableGameObject
{
public:
	virtual void Tick(float DeltaTime) override;
	virtual TStatId GetStatId() const override;
	virtual bool IsTickable() const override;
private:
	GENERATED_BODY()

public:
	UPROPERTY(BlueprintAssignable)
	FOnFinished OnFinished;

	UPROPERTY(BlueprintAssignable)
	FOnUpdated OnUpdated;
	
	/** SceneComponent that will be animated and attached to the TargetComponent*/
	UPROPERTY()
	USceneComponent* SceneComponent = nullptr;
	UPROPERTY()
	FName TargetSocket;

	virtual void Activate() override;

	/**
	 * @brief Lerps a scene component to a new socket.
	 * Useful for socket-to-socket transitions on the same character.
	 * @param CompToAnimate The component we want to lerp smoothly to a new socket
	 * @param InTargetSocket The new attachment parent we want our component to attach to
	 * @param Duration The time it takes for the lerp to complete
	 * @return The async node that will execute the lerping
	 */
	UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true"), Category = "Flow Control")
	static ULerpToNewAttachmentSocketProxy* LerpToNewAttachmentSocket(USceneComponent* CompToAnimate, FName InTargetSocket, float Duration);

	/** Draw a sphere at the current location. */
	UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true"))
	void DrawDebugLocation(float Radius = 10.f, FLinearColor Color = FLinearColor::Red, float DrawDuration = 0.1f);
	
private:
	bool bShouldTick = false;
	
	float Duration;
	float CurrentDuration = 0;

	/** The start transform in relative space of the target socket. */
	FTransform StartTransformRelative;
};

Cpp:

ULerpToNewAttachmentSocketProxy* ULerpToNewAttachmentSocketProxy::LerpToNewAttachmentSocket(USceneComponent* CompToAnimate, FName TargetSocket, float Duration)
{
    ULerpToNewAttachmentSocketProxy* LerpProxy = NewObject<ULerpToNewAttachmentSocketProxy>();
    LerpProxy->SetFlags(RF_StrongRefOnFrame);

    LerpProxy->Duration = Duration;
    LerpProxy->SceneComponent = CompToAnimate;
    LerpProxy->TargetSocket = TargetSocket;

    return LerpProxy;
}

TStatId ULerpToNewAttachmentSocketProxy::GetStatId() const
{
    RETURN_QUICK_DECLARE_CYCLE_STAT(FTest, STATGROUP_Tickables);
}

bool ULerpToNewAttachmentSocketProxy::IsTickable() const
{
    return bShouldTick;
}

void ULerpToNewAttachmentSocketProxy::Activate()
{
    RegisterWithGameInstance(SceneComponent);

    bShouldTick = true;
	
    FName OldSocket = SceneComponent->GetAttachSocketName();
    FString OldSocketString = OldSocket.ToString();
    // immediately attach to the new target socket; we use "KeepWorldTransform" so the component doesn't snap visibility
    SceneComponent->AttachToComponent(SceneComponent->GetAttachParent(), FAttachmentTransformRules::KeepWorldTransform, TargetSocket);
  
    StartTransformRelative = SceneComponent->GetRelativeTransform();
}

void ULerpToNewAttachmentSocketProxy::Tick(float DeltaTime)
{
    float Alpha = FMath::Clamp<float>(CurrentDuration / Duration, 0, 1);

	// the target transform is default initialized as it represents the socket transform
    const FTransform NewTransformInRelative = UKismetMathLibrary::TLerp(StartTransformRelative, FTransform(), Alpha, ELerpInterpolationMode::DualQuatInterp);
    SceneComponent->SetRelativeLocationAndRotation(NewTransformInRelative.GetLocation(), NewTransformInRelative.GetRotation());

    CurrentDuration += DeltaTime;
    OnUpdated.Broadcast();

    if (CurrentDuration >= Duration)
    {
        bShouldTick = false;

        OnFinished.Broadcast();
        SetReadyToDestroy();
    }
}

void ULerpToNewAttachmentSocketProxy::DrawDebugLocation(float Radius, FLinearColor Color, float DrawDuration)
{
    UKismetSystemLibrary::DrawDebugSphere(SceneComponent, SceneComponent->GetComponentLocation(), Radius, 12, Color, DrawDuration);
}