Timelines for Actor Components? [Unreal]

The SimplifiedTimeline

Unreal’s Timelines are Actor Components, so you cannot really use them within other Actor Components as you would do for Actors. Nonetheless, sometimes you might want to create reusable behaviours for Actors using Actor Components, and these behaviours could use some animation (e.g. changing opacity or color of a material over time). This happened to me quite a lot lately, so I thought about a way to overcome the issue. The result is a C++ tool based on Timers, which allows you to have some sort of Timeline in Actor Components! This tool will update a float value over time, following a specified curve.

This UObject can be found with other utilities in this repository, and it is called SimplifiedTimeline: https://github.com/DarioMazzanti/MagicUtilities. I am pasting the current version of the source files also at the bottom of this post.

The following is the most basic use of the SimplifiedTimeline:

  1. Construct the SimplifiedTimeline and store it in a variable
  2. Bind your Events to the TimerUpdated and TimerFinished delegates
  3. Play the SimplifiedTimeline (Reverse is also available)

The next paragraph shows an example of how to use it.

Example: Growing/Shrinking Actor

This is an Actor Component which grows and shrinks an Actor in a loop.

First of all, the SimplifiedTimeline needs to be constructed and stored. For an Actor Component, you could do it on BeginPlay, while for actors also in the Construction Script. You need to specify and Outer object, a playback frequency (in Hz) and an animation curve.
The Outer object is internally required by the SimplifiedTimeline in order to run Timers.
The Playback Frequency specifies at which rate the SimplifiedTimeline will be updated.
The Curve will be used to update the float value over time.

Constructing and storing the SimplifiedTimeline

Then, you can bind your own events to TimerUpdated and TimerFinished. These two events are fired respectively with each SimplifiedTimeline updated and at the end of its execution.

Binding your own events to TimerUpdate and TimerFinished delegates on Begin Play

If you are using the SimplifiedTimeline in an Actor, and initializing it in the Construction Script, you might be able to add custom events from the Events panel of the Details inspector, in your Blueprint.

SimplifiedTimeline events when using the object within an Actor

After defining the custom events which will deal with the timeline delegates, all you need to to is to start the SimplifiedTimeline with the Play function.

Changing the owner actor scale on timeline update
When the timeline finishes, start it again (forward and backward) using the Play and Reverse functions.
Playing the timeline

This is the result once played! The “jump” at the end of the growing phase is given by the custom curve provided to the SimplifiedTimeline:

Source Code:

// HEADER FILE: SimplifiedTimeline.h

#pragma once

#include "CoreMinimal.h"
#include "Engine.h"
#include "UObject/NoExportTypes.h"
#include "Curves/CurveFloat.h"
#include "Engine/EngineTypes.h"
#include "SimplifiedTimeline.generated.h"

UENUM(BlueprintType)
enum class ESimplifiedTimelineDirection : uint8
{
	Forward	UMETA(DisplayName = "Forward"),
	Backward UMETA(DisplayName = "Backward")
};

DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FSimplifiedTimerDelegate, float, CurrentValue, ESimplifiedTimelineDirection, Direction);

UCLASS(BlueprintType)
class MAGICUTILITIES_API USimplifiedTimeline : public UObject
{
	GENERATED_BODY()

private:
	float UpdateInterval = 0.01f;

	ESimplifiedTimelineDirection CurrentDirection = ESimplifiedTimelineDirection::Forward;

	FTimerHandle TimerUpdateHandle;

	UWorld* World = nullptr;

	bool bIsPlaying = false;

	float CurrTime = 0.0f;
	float CurveStartTime;
	float CurveEndTime;
	float CurrentValue;

	void StartTimer();

	UFUNCTION(Category = "Simplified Timeline")
		void UpdateTimer();

public:

	UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Simplified Timeline", Meta = (ExposeOnSpawn = true))
		float PlaybackFrequency = 100;

	UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Simplified Timeline", Meta = (ExposeOnSpawn = true))
		UCurveFloat* Curve;

	UPROPERTY(BlueprintAssignable, Category = "Simplified Timeline")
		FSimplifiedTimerDelegate OnTimerUpdate;

	UPROPERTY(BlueprintAssignable, Category = "Simplified Timeline")
		FSimplifiedTimerDelegate OnTimerFinished;

	// Functions

	UFUNCTION(BlueprintCallable, Category = "Simplified Timeline", meta = (HidePin = "WorldContextObject", DefaultToSelf = "WorldContextObject"))
		void Play(UObject* WorldContextObject);	

	UFUNCTION(BlueprintCallable, Category = "Simplified Timeline")
		void Pause();

	UFUNCTION(BlueprintCallable, Category = "Simplified Timeline")
		void Unpause();

	UFUNCTION(BlueprintCallable, Category = "Simplified Timeline")
		void Stop();

	UFUNCTION(BlueprintCallable, Category = "Simplified Timeline")
		void Reverse();


};
// SOURCE FILE: SimplifiedTimeline.cpp

#include "SimplifiedTimeline.h"

void USimplifiedTimeline::Play(UObject* WorldContextObject)
{
	CurrentDirection = ESimplifiedTimelineDirection::Forward;

	if (!bIsPlaying)
	{
		World = GEngine->GetWorldFromContextObject(WorldContextObject);

		if (World != nullptr && Curve != nullptr)
			StartTimer();

		return;
	}
	else
	{
		switch (CurrentDirection)
		{
		case ESimplifiedTimelineDirection::Forward:
			Unpause();
			break;
		case ESimplifiedTimelineDirection::Backward:
			Pause();
			CurrentDirection = ESimplifiedTimelineDirection::Forward;
			Unpause();
			break;
		default:
			break;
		}
	}
}

void USimplifiedTimeline::StartTimer()
{
	// setup start and finish time.
	Curve->GetTimeRange(CurveStartTime, CurveEndTime);

	switch (CurrentDirection)
	{
	case ESimplifiedTimelineDirection::Forward:
		CurrTime = CurveStartTime;
		break;
	case ESimplifiedTimelineDirection::Backward:
		CurrTime = CurveEndTime;
		break;
	default:
		break;
	}

	// setup update interval based on frequency
	UpdateInterval = 1.0 / PlaybackFrequency;

	World->GetTimerManager().ClearAllTimersForObject(this);
	World->GetTimerManager().SetTimer(TimerUpdateHandle, this, &USimplifiedTimeline::UpdateTimer, UpdateInterval, true);
	
	
	bIsPlaying = true;
}

void USimplifiedTimeline::Pause()
{
	if (bIsPlaying)
	{
		if(!World->GetTimerManager().IsTimerPaused(TimerUpdateHandle))
			World->GetTimerManager().PauseTimer(TimerUpdateHandle);
	}
}

void USimplifiedTimeline::Unpause()
{
	if (bIsPlaying)
	{
		if (World->GetTimerManager().IsTimerPaused(TimerUpdateHandle))
			World->GetTimerManager().UnPauseTimer(TimerUpdateHandle);
	}
}

void USimplifiedTimeline::Stop()
{
	if (bIsPlaying)
	{		
		if (World != nullptr)
		{
			if(World->GetTimerManager().TimerExists(TimerUpdateHandle))
				World->GetTimerManager().ClearTimer(TimerUpdateHandle);

			bIsPlaying = false;			
		}
	}
}

void USimplifiedTimeline::Reverse()
{
	CurrentDirection = ESimplifiedTimelineDirection::Backward;
	if (!bIsPlaying)
	{
		if (World != nullptr && Curve != nullptr)
		{
			CurrTime = CurveEndTime;
			StartTimer();
		}
	}
}

void USimplifiedTimeline::UpdateTimer()
{
	if (!bIsPlaying)
		return;

	bool bHasFinished = false;
	
	switch (CurrentDirection)
	{
	case ESimplifiedTimelineDirection::Forward:
		
		CurrTime += UpdateInterval;
		if (CurrTime >= CurveEndTime)
		{
			CurrTime = CurveEndTime;
			bHasFinished = true;
		}

		break;
	case ESimplifiedTimelineDirection::Backward:

		CurrTime -= UpdateInterval;
		if (CurrTime <= CurveStartTime)
		{
			CurrTime = CurveStartTime;			
			bHasFinished = true;
		}
		break;
	default:
		break;
	}

	CurrentValue = Curve->GetFloatValue(CurrTime);
	OnTimerUpdate.Broadcast(CurrentValue, CurrentDirection);

	if (bHasFinished)
	{
		Stop();
		OnTimerFinished.Broadcast(CurrentValue, CurrentDirection);		
	}
}

dario mazzanti, 2021

Up ↑