Sooner or later, any Unreal Engine developer is destined to realize that those amazing blueprint utility tools named Timelines, those magic pieces of software that allow them to execute asynchronous tasks driven by the evolution of a curve during a fixed time, cannot really be placed anywhere. In fact, they can only be placed inside Actors: that’s because, under the hood, what we are really handling when dealing with Timelines are Timeline Components, and a component can exist only as part of an Actor who has such component attached to itself. Anyway, the ability to define a blueprint piece of code that runs in a specified time and produces certain results according to a user-provided interpolation value is very useful and being restricted to define all our timelines in the Actor blueprint class is a pity. Fortunately, Unreal Engine provides plenty of tools for implementing such functionality anywhere in our blueprint code, but that requires some C++ coding to build.

Understanding timelines

Before starting coding a solution, let’s stop one second to think about why Unreal’s timelines are limited to Actors and cannot be defined in any other blueprint class.

As I mentioned above, whenever we define a Timeline inside an Actor blueprint class, we are in fact creating a TimelineComponent and attach it to the Actor we are defining the timeline for. This component is not visible in the components hierarchy, but it can be found in the Variables section of the blueprint. What is the consequence of this?

  • Timelines are instantiated only once (as soon as the Actor is instantiated) and exist as long as the actor exists.
  • As a consequence, timelines keep their own state: they can be paused in the middle of their execution and resumed even a long time after that.
  • Since the defined timeline is a component and the Timeline blueprint node is not mapped to a function, but to a set of functionalities that you can use to drive your timeline execution, you can define as many curves as you want for your timeline. Such curves are actually stored in the component.

Now, if you absolutely need any of the two previous features, then… just define the timeline from the actor. You can have plenty of solutions that you can implement to link your timeline to the UObject you are working from: for example, you can create a Blueprint Interface with a function that returns a TimelineComponent. Your actor would implement such interface and the UObject that needs the timeline will need a reference to that Actor, that will need to be casted to the interface type in order to provide the timeline you need. No heavy dependencies involved, no hard references to chonky blueprints, everyone is happy.

But if you are just looking for a blueprint-friendly way to define a short-term task that executes a piece of logic every frame, driven by the interpolation of a manually defined curve, then there is definitely a solution that doesn’t involve timelines.

Blueprint Async Actions vs Latent Actions

If you want to implement asynchronous, thread-safe tasks in Unreal Engine, you usually have to pick one way between BlueprintAsyncActions and LatentActions. I will not cover in the detail the differences between these two solutions, because this and this video by Alex Quevillon covered the subject perfectly.

Both the approaches are valid for our purpose, but each one of them has its own limitations. To summarize:

  • If you want to just execute a timeline until the end without modifying its flow after it is started, and you need to be able to call the last Update event in the same frame as the OnEnd event, then you should go for a BlueprintAsyncAction.
  • If you don’t care about firing more than one event in the same frame, but you want to be able to do things like interrupting or reverting the timeline’s flow in the middle of the execution, then you should pick the Latent Actions solution.

Since the purpose of this project is to emulate a Timeline (as much as it is possible), we will go with the Latent Actions approach.

ExecuteTimeline function

Let’s create a Blueprint Function Library and let’s define our timeline function. Since we want to implement multiple input and output execution pins in our blueprint node, we need to define an enum for the inputs and an enum for the outputs.

UENUM(BlueprintType)
enum class ETimelineNodeInputPins : uint8
{
	Play,
	PlayFromStart,
	Stop,
	Reverse,
	ReverseFromEnd
};

UENUM(BlueprintType)
enum class ETimelineNodeOutputPins : uint8
{
	Started,
	Update,
	End
};

UCLASS(meta = (DisplayName = "Blueprint Utils Library"))
class BLUEPRINTUTILS_API UBlueprintUtilsLibrary : public UBlueprintFunctionLibrary
{
	GENERATED_BODY()
	
	UFUNCTION(BlueprintCallable, Category = "Blueprint Utils Library", meta = (WorldContext = "WorldContextObject",
		ExpandEnumAsExecs = "Operation,Result", Latent, LatentInfo = "LatentInfo"))
	static void ExecuteTimeline(UObject* WorldContextObject, FLatentActionInfo LatentInfo, 
		ETimelineNodeInputPins Operation, ETimelineNodeOutputPins& Result, const FRuntimeFloatCurve& FloatCurve,
		float Length, float& Value);
};

Now, we need to define a class which will inherit from FPendingLatentAction and contain the actual logic of the timeline.

class FLatentTimelineAction : public FPendingLatentAction
{
public:
	FLatentTimelineAction(FLatentActionInfo& LatentInfo, 
		const FRuntimeFloatCurve& FloatCurve, ETimelineNodeOutputPins& Output, float Length, float& Value)
		: LatentActionInfo(LatentInfo)
		, FloatCurve(FloatCurve)
		, Output(Output)
		, Length(Length)
		, Value(Value)
	{
		Output = ETimelineNodeOutputPins::Started;
		Time = 0.0f;
		bPlaying = false;
		Playrate = 1.0f;
		bStarted = false;
	}

public:
	FLatentActionInfo LatentActionInfo;
	const FRuntimeFloatCurve& FloatCurve;
	ETimelineNodeOutputPins& Output;

protected:
	float Time;
	float Length;
	float& Value;
	bool bPlaying;
	float Playrate;
	bool bStarted;

public:
	void Play();
	void PlayFromStart();
	void Stop();
	void Reverse();
	void ReverseFromEnd();

protected:
	virtual void UpdateOperation(FLatentResponse& Response) override;

#if WITH_EDITOR
	virtual FString GetDescription() const override
	{
		return FString::Printf(TEXT("Interpolating float curve: %.2f"), FMath::Min(1.0f, Time / Length));
	}
#endif
};

In the FLatentTimelineAction class, the variables responsible for the control of the timeline interpolation are Time, bPlaying and Playrate. While Time keeps track of the current progress on the timeline, Playrate is responsible of the interpolation direction, and bPlaying will always be true as long as the action does not need to be destroyed (which will happen once the interpolation reaches the end of the curve).

void FLatentTimelineAction::Play()
{
	bPlaying = true;
	Playrate = 1.0f;
}

void FLatentTimelineAction::PlayFromStart()
{
	bPlaying = true;
	Playrate = 1.0f;
	Time = 0.0f;
}

void FLatentTimelineAction::Stop()
{
	bPlaying = false;
}

void FLatentTimelineAction::Reverse()
{
	bPlaying = true;
	Playrate = -1.0f;
}

void FLatentTimelineAction::ReverseFromEnd()
{
	bPlaying = true;
	Playrate = -1.0f;
	Time = Length;
}

Finally, the core of the execution is implemented in the UpdateOperation method. Due to the nature of Latent Actions, we are allowed to fire only one output pin every frame: for this reason, the first frame of the execution does not actually start the timeline, since we need to return control to the normal execution flow. After that first frame of assessment, we handle the sampling as shown below, and destroy the timeline only when bPlaying is set to false (either manually by the user or automatically whenever the timeline reaches the end).

void FLatentTimelineAction::UpdateOperation(FLatentResponse& Response)
{
	if (!bStarted)
	{
		bStarted = true;
		Output = ETimelineNodeOutputPins::Started;
		Response.TriggerLink(LatentActionInfo.ExecutionFunction, LatentActionInfo.UUID,
			LatentActionInfo.CallbackTarget);
		return;
	}

	if (!bPlaying)
	{
		Response.FinishAndTriggerIf(true, LatentActionInfo.ExecutionFunction, LatentActionInfo.UUID,
			LatentActionInfo.CallbackTarget);
		return;
	}

	Time += Response.ElapsedTime() * Playrate;
	
	if (Playrate > 0.0f)
	{
		// Moving forward
		if (Time >= Length)
		{
			Output = ETimelineNodeOutputPins::End;
			Value = FloatCurve.GetRichCurveConst()->Eval(Length);
			bPlaying = true;
			Response.TriggerLink(LatentActionInfo.ExecutionFunction, LatentActionInfo.UUID,
				LatentActionInfo.CallbackTarget);
			return;
		}
	}
	else
	{
		// Moving backwards
		if (Time <= 0.0f)
		{
			Output = ETimelineNodeOutputPins::End;
			Value = FloatCurve.GetRichCurveConst()->Eval(0.0f);
			bPlaying = false;
			Response.TriggerLink(LatentActionInfo.ExecutionFunction, LatentActionInfo.UUID,
				LatentActionInfo.CallbackTarget);
			return;
		}
	}

	Value = FloatCurve.GetRichCurveConst()->Eval(Time);
	Output = ETimelineNodeOutputPins::Update;
	Response.TriggerLink(LatentActionInfo.ExecutionFunction, LatentActionInfo.UUID,
		LatentActionInfo.CallbackTarget);
}

A note on interpolation curves

Normally, the most used approach to provide a curve as input is to use the UCurveFloat data asset. Personally, I am not quite a fan of this approach, because it forces you to create an actual asset that resides in your project, and if you decide to not use that timeline anymore, you easily forget about the curve’s existence, and your project ends up being full of unused, deprecated curves. In most cases, you just need to define temporary curves whose reason to exist is exclusively linked to the existence of usually one function. For these kind of situations, I prefer using FRuntimeFloatCurve. This struct allows you to draw the curve directly in the property field, and if you delete that variable, then you also delete the curve. Easy and clean.