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 two additional virtual functions:

  • Tick(float DeltaTime). This gives us the DeltaTime. Due to the class default object we have to implement an early out so that the actual Tick functionality is not called where we don’t need it. Also, 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, and add an additional requirement to our early out section.
  • 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!

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. 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 const FTransform StartTransformInTargetRelative = UKismetMathLibrary::ComposeTransforms(StartTransformWorld, WorldToTargetSocket)ComposeTransforms will apply the first transform, and then the second. By moving to the start transform in world space and then applying the WorldToTargetSocket transform, we receive the start transform in target socket space. Now we have both start and target transforms in target socket space.
    Next comes the actual lerping!
  2. 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 const FTransform NewTransform = UKismetMathLibrary::TLerp(StartTransformRelative, TargetTransformRelative, Alpha, SomeInterpolationMode) function to calculate our new transform in relative space. After that, we update the scene component’s relative location and rotation (this will ignore scale).
    We then check if the current duration has surpassed or is equal to the given duration. If so, we broadcast the OnFinished delegate, stop the object from ticking and allow it to be destroyed. That’s it!

The code

Header:

DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnFinished);

UCLASS()
class YOURMODULE_API ULerpToNewAttachmentSocket : public UBlueprintAsyncActionBase, public FTickableGameObject
{
        GENERATED_BODY()
public:
        // FTickableGameObject interface
	virtual void Tick(float DeltaTime) override;
	virtual TStatId GetStatId() const override;

	UPROPERTY(BlueprintAssignable)
	FOnFinished OnFinished;
	/** SceneComponent that will be animated to the new socket */
	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 parent and position
	 * @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 ULerpToNewAttachmentSocket* LerpToNewAttachmentSocket(USceneComponent* CompToAnimate, FName InTargetSocket, float Duration);
	
private:
	bool bShouldTick = false;

	float Duration;
	float CurrentDuration = 0;

	FTransform StartTransformRelative;
	// default initialized because the socket is supposed to have the correct transform so that the attachment can be at 0
	FTransform TargetTransformRelative;
};

Cpp:

void ULerpToNewAttachmentSocket::Tick(float DeltaTime)
{
    if (bShouldTick == false || HasAnyFlags(RF_ClassDefaultObject))
    {
        return;
    }

    float Alpha = FMath::Clamp<float>(CurrentDuration / Duration, 0, 1);
    const FTransform NewTransform = UKismetMathLibrary::TLerp(StartTransformRelative, TargetTransformRelative, Alpha, ELerpInterpolationMode::DualQuatInterp);
	
    FTransform NewTransformInWorld = UKismetMathLibrary::ComposeTransforms(NewTransform, SceneComponent->GetAttachParent()->GetSocketTransform(TargetSocket));
    UKismetSystemLibrary::DrawDebugSphere(SceneComponent, NewTransformInWorld.GetLocation(), 10, 12, FLinearColor::Red, 0.1);

    SceneComponent->SetRelativeLocationAndRotation(NewTransform.GetLocation(), NewTransform.GetRotation());

    CurrentDuration += DeltaTime;

    if (CurrentDuration >= Duration)
    {
        bShouldTick = false;
        OnFinished.Broadcast();
        SetReadyToDestroy();
    }
}

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

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

    bShouldTick = true;

    FName OldSocket = SceneComponent->GetAttachSocketName();
    FString OldSocketString = OldSocket.ToString();
    // attach at the start to the new target socket, we use transforms to start from the initial position
    SceneComponent->AttachToComponent(SceneComponent->GetAttachParent(), FAttachmentTransformRules::KeepWorldTransform, TargetSocket);

    const FTransform StartTransformWorld = SceneComponent->GetAttachParent()->GetSocketTransform(OldSocket, RTS_World);
    //const FTransform StartTransformWorld = UKismetMathLibrary::ComposeTransforms(StartTransform, StartParentComponent->GetComponentTransform());
    UKismetSystemLibrary::DrawDebugSphere(SceneComponent, StartTransformWorld.GetLocation(), 10, 12, FLinearColor::Green, 2);
    // transform from world into relative from target to keep the initial offset when starting the lerp in tact, and to make it independent of the source component
    // example: picture a knight falling. The different from start to target will always be target location - difference in relative space
    const FTransform StartTransformInTargetRelative = UKismetMathLibrary::ComposeTransforms(StartTransformWorld, SceneComponent->GetAttachParent()->GetSocketTransform(TargetSocket, RTS_World).Inverse());
    StartTransformRelative = StartTransformInTargetRelative;
}

ULerpToNewAttachmentSocket* ULerpToNewAttachmentSocket::LerpToNewAttachmentSocket(USceneComponent* CompToAnimate, FName TargetSocket, float Duration)
{
    ULerpToNewAttachmentSocket* BlueprintNode = NewObject<ULerpToNewAttachmentSocket>();
    BlueprintNode->Duration = Duration;
    BlueprintNode->SceneComponent = CompToAnimate;
    BlueprintNode->TargetSocket = TargetSocket;
    return BlueprintNode;
}
About the Author

German freelancer. Currently contracted to Epic Games. Working on Niagara.

View Articles